diff --git a/docs/Commands List.md b/docs/Commands List.md index 47a7afc3..3537e8b7 100644 --- a/docs/Commands List.md +++ b/docs/Commands List.md @@ -120,6 +120,8 @@ Command 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 priviledges and removes server custom reaction. | `.dcr 5` +`.crstatsclear` | Resets the counters on `.crstats`. You can specify a trigger to clear stats only for that trigger. | `.crstatsclear` or `.crstatsclear rng` +`.crstats` | Shows a list of custom reactions and the number of times they have been executed. Paginated with 10 per page. Use `.crstatsclear` to reset the counters. | `.crstats` or `.crstats 3` ###### [Back to TOC](#table-of-contents) @@ -157,6 +159,8 @@ Command and aliases | Description | Usage `>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` `>pollend` | Stops active poll on this server and prints the results in this channel. **Requires ManageMessages server permission.** | `>pollend` `>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` `>pick` | Picks the currency planted in this channel. | `>pick` `>plant` | Spend a unit of currency to plant it in this channel. (If bot is restarted or crashes, the currency will be lost) | `>plant` `>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` @@ -322,6 +326,7 @@ Command and aliases | Description | Usage `~osu` | Shows osu stats for a player. | `~osu Name` or `~osu Name taiko` `~osub` | Shows information about an osu beatmap. | `~osub https://osu.ppy.sh/s/127712` `~osu5` | Displays a user's top 5 plays. | `~osu5 Name` +`~overwatch` `~ow` | Show's basic stats on a player (competitive rank, playtime, level etc) Region codes are: `eu` `us` `cn` `kr` | `~ow us Battletag#1337` or `~overwatch eu Battletag#2016` `~placelist` | Shows the list of available tags for the `~place` command. | `~placelist` `~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` diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 196aaea5..673d4ae6 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -615,7 +615,9 @@ namespace NadekoBot.Modules.Administration g.GetDefaultChannelAsync() )).ConfigureAwait(false); - await Task.WhenAll(channels.Select(c => c.SendConfirmAsync($"πŸ†• Message from {umsg.Author} `[Bot Owner]`:", message))) + if (channels == null) + return; + await Task.WhenAll(channels.Where(c => c != null).Select(c => c.SendConfirmAsync($"πŸ†• Message from {umsg.Author} `[Bot Owner]`:", message))) .ConfigureAwait(false); await umsg.Channel.SendConfirmAsync("πŸ†—").ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs b/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs index e834e80b..d0a64082 100644 --- a/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/AutoAssignRoleCommands.cs @@ -6,6 +6,7 @@ using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NLog; using System; +using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; @@ -17,9 +18,13 @@ namespace NadekoBot.Modules.Administration public class AutoAssignRoleCommands { private static Logger _log { get; } + //guildid/roleid + private static ConcurrentDictionary AutoAssignedRoles { get; } static AutoAssignRoleCommands() { + AutoAssignedRoles = new ConcurrentDictionary(NadekoBot.AllGuildConfigs.Where(x => x.AutoAssignRoleId != 0) + .ToDictionary(k => k.GuildId, v => v.AutoAssignRoleId)); _log = LogManager.GetCurrentClassLogger(); NadekoBot.Client.UserJoined += (user) => { @@ -27,15 +32,16 @@ namespace NadekoBot.Modules.Administration { try { - GuildConfig conf = NadekoBot.AllGuildConfigs.FirstOrDefault(gc => gc.GuildId == user.Guild.Id); + ulong roleId = 0; + AutoAssignedRoles.TryGetValue(user.Guild.Id, out roleId); - if (conf.AutoAssignRoleId == 0) + if (roleId == 0) return; - var role = user.Guild.Roles.FirstOrDefault(r => r.Id == conf.AutoAssignRoleId); + var role = user.Guild.Roles.FirstOrDefault(r => r.Id == roleId); if (role != null) - await user.AddRolesAsync(role); + await user.AddRolesAsync(role).ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } }); @@ -55,9 +61,16 @@ namespace NadekoBot.Modules.Administration { conf = uow.GuildConfigs.For(channel.Guild.Id, set => set); if (role == null) + { conf.AutoAssignRoleId = 0; + ulong throwaway; + AutoAssignedRoles.TryRemove(channel.Guild.Id, out throwaway); + } else + { conf.AutoAssignRoleId = role.Id; + AutoAssignedRoles.AddOrUpdate(channel.Guild.Id, role.Id, (key, val) => role.Id); + } await uow.CompleteAsync().ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs b/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs index dfd8b0a7..e4e66155 100644 --- a/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs +++ b/src/NadekoBot/Modules/Administration/Commands/SelfAssignedRolesCommand.cs @@ -195,12 +195,8 @@ namespace NadekoBot.Modules.Administration if (conf.AutoDeleteSelfAssignedRoleMessages) { - var t = Task.Run(async () => - { - await Task.Delay(3000).ConfigureAwait(false); - try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } // if 502 or something, i don't want bot crashing - try { await usrMsg.DeleteAsync().ConfigureAwait(false); } catch { } - }); + msg.DeleteAfter(3); + umsg.DeleteAfter(3); } } @@ -242,12 +238,8 @@ namespace NadekoBot.Modules.Administration if (autoDeleteSelfAssignedRoleMessages) { - var t = Task.Run(async () => - { - await Task.Delay(3000).ConfigureAwait(false); - try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } // if 502 or something, i don't want bot crashing - try { await umsg.DeleteAsync().ConfigureAwait(false); } catch { } - }); + msg.DeleteAfter(3); + umsg.DeleteAfter(3); } } } diff --git a/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs b/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs index 1a42e613..beffdd57 100644 --- a/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/ServerGreetCommands.cs @@ -52,11 +52,7 @@ namespace NadekoBot.Modules.Administration var toDelete = await channel.SendMessageAsync(msg.SanitizeMentions()).ConfigureAwait(false); if (conf.AutoDeleteByeMessagesTimer > 0) { - var t = Task.Run(async () => - { - await Task.Delay(conf.AutoDeleteByeMessagesTimer * 1000).ConfigureAwait(false); // 5 minutes - try { await toDelete.DeleteAsync().ConfigureAwait(false); } catch { } - }); + toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); } } catch (Exception ex) { _log.Warn(ex); } @@ -91,11 +87,7 @@ namespace NadekoBot.Modules.Administration var toDelete = await channel.SendMessageAsync(msg.SanitizeMentions()).ConfigureAwait(false); if (conf.AutoDeleteGreetMessagesTimer > 0) { - var t = Task.Run(async () => - { - await Task.Delay(conf.AutoDeleteGreetMessagesTimer * 1000).ConfigureAwait(false); // 5 minutes - try { await toDelete.DeleteAsync().ConfigureAwait(false); } catch { } - }); + toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); } } catch (Exception ex) { _log.Warn(ex); } diff --git a/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs b/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs index 81be7144..0546837a 100644 --- a/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs +++ b/src/NadekoBot/Modules/ClashOfClans/ClashOfClans.cs @@ -11,6 +11,7 @@ using Discord.WebSocket; using NadekoBot.Services.Database.Models; using System.Linq; using NadekoBot.Extensions; +using System.Threading; namespace NadekoBot.Modules.ClashOfClans { @@ -19,6 +20,8 @@ namespace NadekoBot.Modules.ClashOfClans { public static ConcurrentDictionary> ClashWars { get; set; } = new ConcurrentDictionary>(); + private static Timer checkWarTimer { get; } + static ClashOfClans() { using (var uow = DbHandler.UnitOfWork()) @@ -32,13 +35,21 @@ namespace NadekoBot.Modules.ClashOfClans ?.GetTextChannel(cw.ChannelId); return cw; }) - .Where(cw => cw?.Channel != null) + .Where(cw => cw.Channel != null) .GroupBy(cw => cw.GuildId) .ToDictionary(g => g.Key, g => g.ToList())); } - } - public ClashOfClans() : base() - { + + 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) @@ -46,12 +57,21 @@ namespace NadekoBot.Modules.ClashOfClans var Bases = war.Bases; for (var i = 0; i < Bases.Count; i++) { - if (Bases[i].CallUser == null) continue; - if (!Bases[i].BaseDestroyed && DateTime.UtcNow - Bases[i].TimeAdded >= callExpire) + var callUser = Bases[i].CallUser; + if (callUser == null) continue; + if ((!Bases[i].BaseDestroyed) && DateTime.UtcNow - Bases[i].TimeAdded >= callExpire) { - Bases[i] = null; - try { await war.Channel.SendErrorAsync($"β—πŸ”°**Claim from @{Bases[i].CallUser} for a war against {war.ShortPrint()} has expired.**").ConfigureAwait(false); } catch { } - } + if (Bases[i].Stars != 3) + Bases[i].BaseDestroyed = true; + else + Bases[i] = null; + try + { + SaveWar(war); + await war.Channel.SendErrorAsync($"β—πŸ”°**Claim from @{Bases[i].CallUser} for a war against {war.ShortPrint()} has expired.**").ConfigureAwait(false); + } + catch { } + } } } @@ -216,7 +236,7 @@ namespace NadekoBot.Modules.ClashOfClans { var channel = (ITextChannel)umsg.Channel; - var warsInfo = GetWarInfo(umsg,number); + var warsInfo = GetWarInfo(umsg, number); if (warsInfo == null) { await channel.SendErrorAsync("πŸ”° That war does not exist.").ConfigureAwait(false); @@ -359,4 +379,4 @@ namespace NadekoBot.Modules.ClashOfClans } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index d7c28f91..d68aeab6 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -115,7 +115,7 @@ namespace NadekoBot.Modules.CustomReactions reactions.Add(cr); } - await imsg.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await imsg.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithTitle("New Custom Reaction") .WithDescription($"#{cr.Id}") .AddField(efb => efb.WithName("Trigger").WithValue(key)) @@ -225,7 +225,7 @@ namespace NadekoBot.Modules.CustomReactions await imsg.Channel.SendErrorAsync("No custom reaction found with that id.").ConfigureAwait(false); else { - await imsg.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await imsg.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithDescription($"#{id}") .AddField(efb => efb.WithName("Trigger").WithValue(found.Trigger)) .AddField(efb => efb.WithName("Response").WithValue(found.Response + "\n```css\n" + found.Response + "```")) @@ -304,7 +304,7 @@ namespace NadekoBot.Modules.CustomReactions await imsg.Channel.EmbedAsync(ReactionStats.OrderByDescending(x => x.Value) .Skip((page - 1)*9) .Take(9) - .Aggregate(new EmbedBuilder().WithColor(NadekoBot.OkColor).WithTitle($"Custom Reaction stats page #{page}"), + .Aggregate(new EmbedBuilder().WithOkColor().WithTitle($"Custom Reaction stats page #{page}"), (agg, cur) => agg.AddField(efb => efb.WithName(cur.Key).WithValue(cur.Value.ToString()).WithIsInline(true))) .Build()) .ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs b/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs index 22d58035..7b2c8118 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/DiceRollCommand.cs @@ -46,54 +46,52 @@ namespace NadekoBot.Modules.Gambling await channel.SendFileAsync(imageStream, "dice.png", $"{umsg.Author.Mention} rolled " + Format.Code(gen.ToString())).ConfigureAwait(false); } - //todo merge into internallDndRoll and internalRoll - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task Roll(IUserMessage umsg, string arg) + + public enum RollOrderType { - var channel = (ITextChannel)umsg.Channel; - if (channel == null) - return; - - var ordered = true; - var rng = new NadekoRandom(); - Match match; - if ((match = dndRegex.Match(arg)).Length != 0) - { - int n1; - int n2; - if (int.TryParse(match.Groups["n1"].ToString(), out n1) && - int.TryParse(match.Groups["n2"].ToString(), out n2) && - n1 <= 50 && n2 <= 100000 && n1 > 0 && n2 > 0) - { - var add = 0; - var sub = 0; - int.TryParse(match.Groups["add"].Value, out add); - int.TryParse(match.Groups["sub"].Value, out sub); - - var arr = new int[n1]; - for (int i = 0; i < n1; i++) - { - arr[i] = rng.Next(1, n2 + 1) + add - sub; - } - var elemCnt = 0; - await channel.SendConfirmAsync($"{umsg.Author.Mention} rolled {n1} {(n1 == 1 ? "die" : "dice")} `1 to {n2}` +`{add}` -`{sub}`.\n`Result:` " + string.Join(", ", (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x => elemCnt++ % 2 == 0 ? $"**{x}**" : x.ToString()))).ConfigureAwait(false); - } - } + Ordered, + Unordered } [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] [Priority(0)] public async Task Roll(IUserMessage umsg, int num) + { + await InternalRoll(umsg, num, true).ConfigureAwait(false); + } + + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public async Task Rolluo(IUserMessage umsg, int num) + { + await InternalRoll(umsg, num, false).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task Roll(IUserMessage umsg, string arg) + { + await InternallDndRoll(umsg, arg, true).ConfigureAwait(false); + } + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task Rolluo(IUserMessage umsg, string arg) + { + await InternallDndRoll(umsg, arg, false).ConfigureAwait(false); + } + + private async Task InternalRoll(IUserMessage umsg, int num, bool ordered) { var channel = (ITextChannel)umsg.Channel; if (channel == null) return; - var ordered = true; - if (num < 1 || num > 30) { await channel.SendErrorAsync("Invalid number specified. You can roll up to 1-30 dice at a time.").ConfigureAwait(false); @@ -137,15 +135,12 @@ namespace NadekoBot.Modules.Gambling await channel.SendFileAsync(ms, "dice.png", $"{umsg.Author.Mention} rolled {values.Count} {(values.Count == 1 ? "die" : "dice")}. Total: **{values.Sum()}** Average: **{(values.Sum() / (1.0f * values.Count)).ToString("N2")}**").ConfigureAwait(false); } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Rolluo(IUserMessage umsg, string arg) + private async Task InternallDndRoll(IUserMessage umsg, string arg, bool ordered) { var channel = (ITextChannel)umsg.Channel; if (channel == null) return; - var ordered = false; var rng = new NadekoRandom(); Match match; if ((match = dndRegex.Match(arg)).Length != 0) @@ -172,59 +167,6 @@ namespace NadekoBot.Modules.Gambling } } - [NadekoCommand, Usage, Description, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Rolluo(IUserMessage umsg, int num) - { - var channel = (ITextChannel)umsg.Channel; - if (channel == null) - return; - - var ordered = false; - - if (num < 1 || num > 30) - { - await channel.SendErrorAsync("Invalid number specified. You can roll up to 1-30 dice at a time.").ConfigureAwait(false); - return; - } - - var rng = new NadekoRandom(); - - var dice = new List(num); - var values = new List(num); - for (var i = 0; i < num; i++) - { - var randomNumber = rng.Next(1, 7); - var toInsert = dice.Count; - if (ordered) - { - if (randomNumber == 6 || dice.Count == 0) - toInsert = 0; - else if (randomNumber != 1) - for (var j = 0; j < dice.Count; j++) - { - if (values[j] < randomNumber) - { - toInsert = j; - break; - } - } - } - else - { - toInsert = dice.Count; - } - dice.Insert(toInsert, GetDice(randomNumber)); - values.Insert(toInsert, randomNumber); - } - - var bitmap = dice.Merge(); - var ms = new MemoryStream(); - bitmap.SaveAsPng(ms); - ms.Position = 0; - await channel.SendFileAsync(ms, "dice.png", $"{umsg.Author.Mention} rolled {values.Count} {(values.Count == 1 ? "die" : "dice")}. Total: **{values.Sum()}** Average: **{(values.Sum() / (1.0f * values.Count)).ToString("N2")}**").ConfigureAwait(false); - } - [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] public async Task NRoll(IUserMessage umsg, [Remainder] string range) @@ -241,7 +183,7 @@ namespace NadekoBot.Modules.Gambling .Select(int.Parse) .ToArray(); if (arr[0] > arr[1]) - throw new ArgumentException("First argument should be bigger than the second one."); + throw new ArgumentException("Second argument must be larger than the first one."); rolled = new NadekoRandom().Next(arr[0], arr[1] + 1); } else diff --git a/src/NadekoBot/Modules/Gambling/Commands/DrawCommand.cs b/src/NadekoBot/Modules/Gambling/Commands/DrawCommand.cs index 58785882..0f76ce41 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/DrawCommand.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/DrawCommand.cs @@ -53,7 +53,7 @@ namespace NadekoBot.Modules.Gambling MemoryStream bitmapStream = new MemoryStream(); images.Merge().SaveAsPng(bitmapStream); bitmapStream.Position = 0; - //todo CARD NAMES? + var toSend = $"{msg.Author.Mention}"; if (cardObjects.Count == 5) toSend += $" drew `{Cards.GetHandValue(cardObjects)}`"; diff --git a/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs b/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs index 867db68e..110905cc 100644 --- a/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs +++ b/src/NadekoBot/Modules/Gambling/Commands/FlipCoinCommand.cs @@ -63,20 +63,11 @@ namespace NadekoBot.Modules.Gambling .ConfigureAwait(false); return; } - // todo update this - long userFlowers; - using (var uow = DbHandler.UnitOfWork()) + var removed = await CurrencyHandler.RemoveCurrencyAsync(guildUser, "Betflip Gamble", amount, false).ConfigureAwait(false); + if (!removed) { - userFlowers = uow.Currency.GetOrCreate(umsg.Author.Id).Amount; + await channel.SendErrorAsync($"{guildUser.Mention} You don't have enough {Gambling.CurrencyPluralName}.").ConfigureAwait(false); } - - if (userFlowers < amount) - { - await channel.SendErrorAsync($"{umsg.Author.Mention} You don't have enough {Gambling.CurrencyPluralName}. You only have {userFlowers}{Gambling.CurrencySign}.").ConfigureAwait(false); - return; - } - - await CurrencyHandler.RemoveCurrencyAsync(guildUser, "Betflip Gamble", amount, false).ConfigureAwait(false); //heads = true //tails = false diff --git a/src/NadekoBot/Modules/Games/Commands/Acropobia.cs b/src/NadekoBot/Modules/Games/Commands/Acropobia.cs new file mode 100644 index 00000000..795c0f63 --- /dev/null +++ b/src/NadekoBot/Modules/Games/Commands/Acropobia.cs @@ -0,0 +1,288 @@ +ο»Ώusing Discord; +using Discord.Commands; +using NadekoBot.Attributes; +using NadekoBot.Extensions; +using NadekoBot.Services; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Games +{ + public partial class Games + { + [Group] + public class Acropobia + { + //channelId, game + public static ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); + + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Acro(IUserMessage imsg, int time = 60) + { + var channel = (ITextChannel)imsg.Channel; + + var game = new AcrophobiaGame(channel, time); + if (AcrophobiaGames.TryAdd(channel.Id, game)) + { + try + { + await game.Run(); + } + finally + { + game.EnsureStopped(); + AcrophobiaGames.TryRemove(channel.Id, out game); + } + } + else + { + await channel.SendErrorAsync("Acrophobia game is already running in this channel.").ConfigureAwait(false); + } + } + } + + public enum AcroPhase + { + Submitting, + Idle, // used to wait for some other actions while transitioning through phases + Voting + } + + 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 usersWhoVoted = new ConcurrentHashSet(); + + private int spamCount = 0; + + //text, votes + private readonly ConcurrentDictionary votes = new ConcurrentDictionary(); + + public AcrophobiaGame(ITextChannel channel, int time) + { + this.channel = channel; + this.time = time; + this.source = new CancellationTokenSource(); + + this.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 EmbedBuilder GetEmbed() + { + var i = 0; + return phase == AcroPhase.Submitting + + ? new EmbedBuilder().WithOkColor() + .WithTitle("Acrophobia") + .WithDescription($"Game started. Create a sentence with the following acronym: **{string.Join(".", startingLetters)}.**\n") + .WithFooter(efb => efb.WithText("You have " + this.time + " seconds to make a submission.")) + + : new EmbedBuilder() + .WithOkColor() + .WithTitle("Acrophobia - Submissions Closed") + .WithDescription($@"Acronym was **{string.Join(".", startingLetters)}.** +-- +{this.submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.ToLowerInvariant().ToTitleCase()}**\n")} +--") + .WithFooter(efb => efb.WithText("Vote by typing a number of the submission")); + } + + public async Task Run() + { + NadekoBot.Client.MessageReceived += PotentialAcro; + var embed = GetEmbed(); + + //SUBMISSIONS PHASE + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); + try + { + await Task.Delay(time * 1000, source.Token).ConfigureAwait(false); + phase = AcroPhase.Idle; + } + catch (OperationCanceledException) + { + return; + } + + var i = 0; + if (submissions.Count == 0) + { + await channel.SendErrorAsync("Acrophobia", "Game ended with no submissions."); + return; + } + else if (submissions.Count == 1) + { + await channel.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithDescription($"{submissions.First().Value.Mention} is the winner for being the only user who made a submission!") + .WithFooter(efb => efb.WithText(submissions.First().Key.ToLowerInvariant().ToTitleCase())) + .Build()).ConfigureAwait(false); + return; + } + var submissionClosedEmbed = GetEmbed(); + + await channel.EmbedAsync(submissionClosedEmbed.Build()).ConfigureAwait(false); + + //VOTING PHASE + this.phase = AcroPhase.Voting; + try + { + //30 secondds for voting + await Task.Delay(30000, source.Token).ConfigureAwait(false); + this.phase = AcroPhase.Idle; + } + catch (OperationCanceledException) + { + return; + } + await End().ConfigureAwait(false); + } + + private Task PotentialAcro(IMessage arg) + { + var t = Task.Run(async () => + { + try + { + var msg = arg as IUserMessage; + 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().Build()).ConfigureAwait(false); } + catch { } + } + //user didn't input something already + IGuildUser throwaway; + if (submissions.TryGetValue(input, out throwaway)) + return; + 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; + } + + //try adding it to the list of answers + if (!submissions.TryAdd(input, guildUser)) + return; + + // all good. valid input. answer recorded + await channel.SendConfirmAsync("Acrophobia", $"{guildUser.Mention} submitted their sentence. ({submissions.Count} total)"); + 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().Build()).ConfigureAwait(false); } + catch { } + } + + IGuildUser usr; + //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(); + 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("Acrophobia", $"{guildUser.Mention} cast their vote!").ConfigureAwait(false); + await msg.DeleteAsync().ConfigureAwait(false); + return; + } + + } + } + catch { } + }); + return Task.CompletedTask; + } + + public async Task End() + { + if (!votes.Any()) + { + await channel.SendErrorAsync("Acrophobia", "No votes cast. Game ended with no winner.").ConfigureAwait(false); + return; + } + var table = votes.OrderByDescending(v => v.Value); + var winner = table.First(); + var embed = new EmbedBuilder().WithOkColor() + .WithTitle("Acrophobia") + .WithDescription($"Winner is {submissions[winner.Key].Mention} with {winner.Value} points.\n") + .WithFooter(efb => efb.WithText(winner.Key.ToLowerInvariant().ToTitleCase())); + + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); + } + + public void EnsureStopped() + { + NadekoBot.Client.MessageReceived -= PotentialAcro; + if (!source.IsCancellationRequested) + source.Cancel(); + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs index 575e8edf..048f51f3 100644 --- a/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Hangman/HangmanGame.cs @@ -118,9 +118,9 @@ namespace NadekoBot.Modules.Games.Commands.Hangman .AddField(efb => efb.WithName("It was").WithValue(Term.Word)) .WithImage(eib => eib.WithUrl(Term.ImageUrl)); if (Errors >= MaxErrors) - await GameChannel.EmbedAsync(embed.WithColor(NadekoBot.ErrorColor).Build()).ConfigureAwait(false); + await GameChannel.EmbedAsync(embed.WithErrorColor().Build()).ConfigureAwait(false); else - await GameChannel.EmbedAsync(embed.WithColor(NadekoBot.OkColor).Build()).ConfigureAwait(false); + await GameChannel.EmbedAsync(embed.WithOkColor().Build()).ConfigureAwait(false); } private Task PotentialGuess(IMessage msg) @@ -143,10 +143,8 @@ namespace NadekoBot.Modules.Games.Commands.Hangman if (!(char.IsLetter(msg.Content[0]) || char.IsDigit(msg.Content[0])))// and a letter or a digit return Task.CompletedTask; - var guess = char.ToUpperInvariant(msg.Content[0]); - // todo hmmmm - // how do i want to limit the users on guessing? - // one guess every 5 seconds if wrong? + var guess = char.ToUpperInvariant(msg.Content[0]); + Task.Run(async () => { try diff --git a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs index 61a9a10d..200b7ba5 100644 --- a/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PlantAndPickCommands.cs @@ -17,8 +17,6 @@ using System.Threading.Tasks; namespace NadekoBot.Modules.Games { - //todo make currency generation change and cooldown modifyable - //only by bot owner through commands public partial class Games { /// @@ -119,11 +117,7 @@ namespace NadekoBot.Modules.Games await CurrencyHandler.AddCurrencyAsync((IGuildUser)imsg.Author, "Picked flower(s).", msgs.Count, false).ConfigureAwait(false); var msg = await channel.SendConfirmAsync($"**{imsg.Author}** picked {msgs.Count}{Gambling.Gambling.CurrencySign}!").ConfigureAwait(false); - var t = Task.Run(async () => - { - await Task.Delay(10000).ConfigureAwait(false); - try { await msg.DeleteAsync().ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } - }); + msg.DeleteAfter(10); } [NadekoCommand, Usage, Description, Aliases] diff --git a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs index 840abdf1..23441b6e 100644 --- a/src/NadekoBot/Modules/Games/Commands/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/Commands/PollCommands.cs @@ -172,8 +172,7 @@ namespace NadekoBot.Modules.Games else { var toDelete = await ch.SendConfirmAsync($"{msg.Author.Mention} cast their vote.").ConfigureAwait(false); - await Task.Delay(5000); - await toDelete.DeleteAsync().ConfigureAwait(false); + toDelete.DeleteAfter(5); } } } diff --git a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs index 534c5040..7a2f15c5 100644 --- a/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Commands/Trivia/TriviaGame.cs @@ -111,7 +111,7 @@ namespace NadekoBot.Modules.Games.Trivia Games.TriviaCommands.RunningTrivias.TryRemove(channel.Guild.Id, out throwaway); try { - await channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithTitle("Leaderboard") .WithDescription(GetLeaderboard()) .Build(), "Trivia game ended.").ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index 592881d1..8272598a 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -49,7 +49,7 @@ namespace NadekoBot.Modules.Games return; var rng = new NadekoRandom(); - await channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await channel.EmbedAsync(new EmbedBuilder().WithOkColor() .AddField(efb => efb.WithName("❓ Question").WithValue(question).WithIsInline(false)) .AddField(efb => efb.WithName("🎱 8Ball").WithValue(_8BallResponses.Shuffle().FirstOrDefault()).WithIsInline(false)) .Build()); diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 2b824111..2f25f31a 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -98,7 +98,7 @@ namespace NadekoBot.Modules.Help var embed = new EmbedBuilder() .AddField(fb => fb.WithIndex(1).WithName(str).WithValue($"{ string.Format(com.Summary, com.Module.Prefix)} { GetCommandRequirements(com)}").WithIsInline(true)) .AddField(fb => fb.WithIndex(2).WithName("**Usage**").WithValue($"{string.Format(com.Remarks, com.Module.Prefix)}").WithIsInline(false)) - .WithColor(NadekoBot.OkColor); + .WithOkColor(); await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Music/Classes/MusicControls.cs b/src/NadekoBot/Modules/Music/Classes/MusicControls.cs index 8555d3a3..b70c4d0e 100644 --- a/src/NadekoBot/Modules/Music/Classes/MusicControls.cs +++ b/src/NadekoBot/Modules/Music/Classes/MusicControls.cs @@ -41,8 +41,8 @@ namespace NadekoBot.Modules.Music.Classes public float Volume { get; private set; } - public event EventHandler OnCompleted = delegate { }; - public event EventHandler OnStarted = delegate { }; + public event Action OnCompleted = delegate { }; + public event Action OnStarted = delegate { }; public event Action OnPauseChanged = delegate { }; public IVoiceChannel PlaybackVoiceChannel { get; private set; } @@ -130,6 +130,7 @@ namespace NadekoBot.Modules.Music.Classes { Console.WriteLine("Music thread almost crashed."); Console.WriteLine(ex); + await Task.Delay(3000).ConfigureAwait(false); } finally { @@ -250,11 +251,11 @@ namespace NadekoBot.Modules.Music.Classes { var curSong = CurrentSong; var toUpdate = playlist.Where(s => s.SongInfo.ProviderType == MusicType.Normal && - s.TotalLength == TimeSpan.Zero); + s.TotalTime == TimeSpan.Zero); if (curSong != null) toUpdate = toUpdate.Append(curSong); var ids = toUpdate.Select(s => s.SongInfo.Query.Substring(s.SongInfo.Query.LastIndexOf("?v=") + 3)) - .Distinct(); + .Distinct(); var durations = await NadekoBot.Google.GetVideoDurationsAsync(ids); @@ -264,11 +265,12 @@ namespace NadekoBot.Modules.Music.Classes { if (s.SongInfo.Query.EndsWith(kvp.Key)) { - s.TotalLength = kvp.Value; + s.TotalTime = kvp.Value; return; } } }); + } public void Destroy() diff --git a/src/NadekoBot/Modules/Music/Classes/MusicExtensions.cs b/src/NadekoBot/Modules/Music/Classes/MusicExtensions.cs new file mode 100644 index 00000000..2840fd16 --- /dev/null +++ b/src/NadekoBot/Modules/Music/Classes/MusicExtensions.cs @@ -0,0 +1,15 @@ +ο»Ώusing Discord; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NadekoBot.Modules.Music.Classes +{ + public static class MusicExtensions + { + public static EmbedAuthorBuilder WithMusicIcon(this EmbedAuthorBuilder eab) => + eab.WithIconUrl("http://i.imgur.com/nhKS3PT.png"); + } +} diff --git a/src/NadekoBot/Modules/Music/Classes/Song.cs b/src/NadekoBot/Modules/Music/Classes/Song.cs index 8a54f0d3..4ed163fa 100644 --- a/src/NadekoBot/Modules/Music/Classes/Song.cs +++ b/src/NadekoBot/Modules/Music/Classes/Song.cs @@ -18,42 +18,20 @@ namespace NadekoBot.Modules.Music.Classes { public string Provider { get; set; } public MusicType ProviderType { get; set; } - /// - /// Will be set only if the providertype is normal - /// public string Query { get; set; } public string Title { get; set; } public string Uri { get; set; } - public string AlbumArt { get; set; } + public string AlbumArt { get; set; } } + public class Song { - public StreamState State { get; set; } - public string PrettyName => - $"**{SongInfo.Title.TrimTo(55)} `{(SongInfo.Provider ?? "-")} by {QueuerName}`**"; - //$"{SongInfo.Title.TrimTo(70)}"; public SongInfo SongInfo { get; } public MusicPlayer MusicPlayer { get; set; } - - public string PrettyUser => - $"{QueuerName}"; public string QueuerName { get; set; } - - public string PrettyProvider => - $"{(SongInfo.Provider ?? "No Provider")}"; - public string PrettyCurrentTime() - { - var time = TimeSpan.FromSeconds(bytesSent / 3840 / 50); - var str = $"{(int)time.TotalMinutes}m {time.Seconds}s / "; - if (TotalLength == TimeSpan.Zero) - str += "**?**"; - else if (TotalLength == TimeSpan.MaxValue) - str += "**∞**"; - else - str += $"{(int)TotalLength.TotalMinutes}m {TotalLength.Seconds}s"; - return str; - } + public TimeSpan TotalTime { get; set; } = TimeSpan.Zero; + public TimeSpan CurrentTime => TimeSpan.FromSeconds(bytesSent / frameBytes / (1000 / milliseconds)); const int milliseconds = 20; const int samplesPerFrame = (48000 / 1000) * milliseconds; @@ -61,11 +39,33 @@ namespace NadekoBot.Modules.Music.Classes private ulong bytesSent { get; set; } = 0; - public bool PrintStatusMessage { get; set; } = true; + //pwetty + + public string PrettyProvider => + $"{(SongInfo.Provider ?? "No Provider")}"; + + public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime; + + public string PrettyName => $"**[{SongInfo.Title.TrimTo(70)}]({SongInfo.Query})**"; + + public string PrettyInfo => $"{PrettyTotalTime} | {PrettyProvider} | {QueuerName}"; + + public string PrettyFullName => $"{PrettyName}\n\t\t*{PrettyInfo}*"; + + public string PrettyCurrentTime => CurrentTime.ToString(@"mm\:ss"); + + private string PrettyTotalTime { + get { + if (TotalTime == TimeSpan.Zero) + return "(?)"; + else if (TotalTime == TimeSpan.MaxValue) + return "**∞**"; + else + return TotalTime.ToString(@"mm\:ss"); + } + } private int skipTo = 0; - private Logger _log; - public int SkipTo { get { return skipTo; } set { @@ -74,7 +74,7 @@ namespace NadekoBot.Modules.Music.Classes } } - public TimeSpan TotalLength { get; set; } = TimeSpan.Zero; + private readonly Logger _log; public Song(SongInfo songInfo) { @@ -86,16 +86,9 @@ namespace NadekoBot.Modules.Music.Classes { var s = new Song(SongInfo); s.MusicPlayer = MusicPlayer; - s.State = StreamState.Queued; return s; } - public Song SetMusicPlayer(MusicPlayer mp) - { - this.MusicPlayer = mp; - return this; - } - public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) { var filename = Path.Combine(Music.MusicDataPath, DateTime.Now.UnixTimestamp().ToString()); @@ -105,7 +98,7 @@ namespace NadekoBot.Modules.Music.Classes try { - var attempt = 0; + var attempt = 0; var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy var finished = false; @@ -126,7 +119,7 @@ namespace NadekoBot.Modules.Music.Classes _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."); @@ -136,7 +129,7 @@ namespace NadekoBot.Modules.Music.Classes { continue; } - } + } else if (prebufferingTask.IsCanceled) { _log.Debug("Prebuffering canceled. Cannot get any data from the stream."); @@ -145,7 +138,7 @@ namespace NadekoBot.Modules.Music.Classes finished = true; } sw.Stop(); - _log.Debug("Prebuffering successfully completed in "+ sw.Elapsed); + _log.Debug("Prebuffering successfully completed in " + sw.Elapsed); var outStream = voiceClient.CreatePCMStream(960); @@ -157,7 +150,7 @@ namespace NadekoBot.Modules.Music.Classes //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) + if (read < frameBytes) _log.Debug("read {0}", read); unchecked { @@ -177,10 +170,15 @@ namespace NadekoBot.Modules.Music.Classes 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; @@ -189,7 +187,10 @@ namespace NadekoBot.Modules.Music.Classes attempt = 0; while (this.MusicPlayer.Paused) + { await Task.Delay(200, cancelToken).ConfigureAwait(false); + nextTime = Environment.TickCount + milliseconds; + } buffer = AdjustVolume(buffer, MusicPlayer.Volume); @@ -204,7 +205,7 @@ namespace NadekoBot.Modules.Music.Classes finally { await bufferTask; - if(inStream != null) + if (inStream != null) inStream.Dispose(); } } @@ -218,35 +219,6 @@ namespace NadekoBot.Modules.Music.Classes _log.Debug("Buffering successfull"); } - /* - //stackoverflow ftw - private static byte[] AdjustVolume(byte[] audioSamples, float volume) - { - if (Math.Abs(volume - 1.0f) < 0.01f) - return audioSamples; - var array = new byte[audioSamples.Length]; - for (var i = 0; i < array.Length; i += 2) - { - - // convert byte pair to int - short buf1 = audioSamples[i + 1]; - short buf2 = audioSamples[i]; - - buf1 = (short)((buf1 & 0xff) << 8); - buf2 = (short)(buf2 & 0xff); - - var res = (short)(buf1 | buf2); - res = (short)(res * volume); - - // convert back - array[i] = (byte)res; - array[i + 1] = (byte)(res >> 8); - - } - return array; - } - */ - //aidiakapi ftw public unsafe static byte[] AdjustVolume(byte[] audioSamples, float volume) { @@ -272,202 +244,5 @@ namespace NadekoBot.Modules.Music.Classes return audioSamples; } - - 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 - }) - { TotalLength = TimeSpan.MaxValue }; - } - 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, - }) - { TotalLength = 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.Normal, - Query = svideo.TrackLink, - AlbumArt = svideo.artwork_url, - }) - { TotalLength = 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 = video.Uri, - Query = link, - ProviderType = musicType, - }); - song.SkipTo = gotoTime; - return song; - } - catch (Exception ex) - { - Console.WriteLine($"Failed resolving the link.{ex.Message}"); - 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 - { - Console.WriteLine($"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 - { - Console.WriteLine($"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 - { - Console.WriteLine($"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 - { - Console.WriteLine($"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")); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Classes/SongBuffer.cs b/src/NadekoBot/Modules/Music/Classes/SongBuffer.cs index 0b8e7849..9fd23294 100644 --- a/src/NadekoBot/Modules/Music/Classes/SongBuffer.cs +++ b/src/NadekoBot/Modules/Music/Classes/SongBuffer.cs @@ -25,21 +25,21 @@ namespace NadekoBot.Modules.Music.Classes _log = LogManager.GetCurrentClassLogger(); } - MusicPlayer MusicPlayer; + MusicPlayer MusicPlayer { get; } - private string Basename; + private string Basename { get; } - private SongInfo SongInfo; + private SongInfo SongInfo { get; } - private int SkipTo; + private int SkipTo { get; } - private int MaxFileSize = 2.MiB(); + private int MaxFileSize { get; } = 2.MiB(); private long FileNumber = -1; private long NextFileToRead = 0; - public bool BufferingCompleted { get; private set;} = false; + public bool BufferingCompleted { get; private set; } = false; private ulong CurrentBufferSize = 0; @@ -76,7 +76,8 @@ namespace NadekoBot.Modules.Music.Classes try { outStream.Dispose(); - }catch { } + } + catch { } outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read); currentFileSize = bytesRead; } @@ -108,8 +109,8 @@ Check the guides for your platform on how to setup ffmpeg correctly: } finally { - if(outStream != null) - outStream.Dispose(); + if (outStream != null) + outStream.Dispose(); Console.WriteLine($"Buffering done."); if (p != null) { @@ -130,7 +131,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: private string GetNextFile() { string filename = Basename + "-" + NextFileToRead; - + if (NextFileToRead != 0) { try @@ -151,7 +152,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: private void CleanFiles() { - for (long i = NextFileToRead - 1 ; i <= FileNumber; i++) + for (long i = NextFileToRead - 1; i <= FileNumber; i++) { try { @@ -169,7 +170,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: public override bool CanWrite => false; - public override long Length => (long) CurrentBufferSize; + public override long Length => (long)CurrentBufferSize; public override long Position { get; set; } = 0; @@ -178,7 +179,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: public override int Read(byte[] buffer, int offset, int count) { int read = CurrentFileStream.Read(buffer, offset, count); - if(read < count) + if (read < count) { if (!BufferingCompleted || IsNextFileReady()) { @@ -215,4 +216,4 @@ Check the guides for your platform on how to setup ffmpeg correctly: base.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Classes/SongHandler.cs b/src/NadekoBot/Modules/Music/Classes/SongHandler.cs new file mode 100644 index 00000000..50166561 --- /dev/null +++ b/src/NadekoBot/Modules/Music/Classes/SongHandler.cs @@ -0,0 +1,212 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using VideoLibrary; + +namespace NadekoBot.Modules.Music.Classes +{ + public static class SongHandler + { + 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 }; + } + 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.Normal, + 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 = video.Uri, + Query = link, + ProviderType = musicType, + }); + song.SkipTo = gotoTime; + return song; + } + catch (Exception ex) + { + Console.WriteLine($"Failed resolving the link.{ex.Message}"); + 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 + { + Console.WriteLine($"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 + { + Console.WriteLine($"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 + { + Console.WriteLine($"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 + { + Console.WriteLine($"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/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 81e5c95d..afc42496 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -15,6 +15,7 @@ using Newtonsoft.Json.Linq; using System.Collections.Generic; using NadekoBot.Services.Database.Models; using System.Text.RegularExpressions; +using System.Threading; namespace NadekoBot.Modules.Music { @@ -29,13 +30,13 @@ namespace NadekoBot.Modules.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; + + NadekoBot.Client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; Directory.CreateDirectory(MusicDataPath); } - - private Task Client_UserVoiceStateUpdated(IUser iusr, IVoiceState oldState, IVoiceState newState) + + private Task Client_UserVoiceStateUpdated(IUser iusr, IVoiceState oldState, IVoiceState newState) { var usr = iusr as IGuildUser; if (usr == null || @@ -98,16 +99,17 @@ namespace NadekoBot.Modules.Music [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public Task Destroy(IUserMessage umsg) + public async Task Destroy(IUserMessage umsg) { var channel = (ITextChannel)umsg.Channel; + await channel.SendErrorAsync("This command is temporarily disabled.").ConfigureAwait(false); - MusicPlayer musicPlayer; + /*MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(channel.Guild.Id, out musicPlayer)) return Task.CompletedTask; if (((IGuildUser)umsg.Author).VoiceChannel == musicPlayer.PlaybackVoiceChannel) if(MusicPlayers.TryRemove(channel.Guild.Id, out musicPlayer)) musicPlayer.Destroy(); - return Task.CompletedTask; + return Task.CompletedTask;*/ } [NadekoCommand, Usage, Description, Aliases] @@ -133,8 +135,7 @@ namespace NadekoBot.Modules.Music await QueueSong(((IGuildUser)umsg.Author), channel, ((IGuildUser)umsg.Author).VoiceChannel, query).ConfigureAwait(false); if (channel.Guild.GetCurrentUser().GetPermissions(channel).ManageMessages) { - await Task.Delay(10000).ConfigureAwait(false); - await ((IUserMessage)umsg).DeleteAsync().ConfigureAwait(false); + umsg.DeleteAfter(10); } } @@ -147,8 +148,7 @@ namespace NadekoBot.Modules.Music await QueueSong(((IGuildUser)umsg.Author), channel, ((IGuildUser)umsg.Author).VoiceChannel, query, musicType: MusicType.Soundcloud).ConfigureAwait(false); if (channel.Guild.GetCurrentUser().GetPermissions(channel).ManageMessages) { - await Task.Delay(10000).ConfigureAwait(false); - await ((IUserMessage)umsg).DeleteAsync().ConfigureAwait(false); + umsg.DeleteAfter(10); } } @@ -168,28 +168,40 @@ namespace NadekoBot.Modules.Music var currentSong = musicPlayer.CurrentSong; if (currentSong == null) - return; - - if (currentSong.TotalLength == TimeSpan.Zero) { - await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); + await channel.SendErrorAsync("🎡 No active music player.").ConfigureAwait(false); + return; } - //var toSend = $"🎡 Currently Playing {currentSong.PrettyName} " + $"`{currentSong.PrettyCurrentTime()}`\n"; - var toSend = $"🎡 Currently Playing {currentSong.PrettyName}\n"; - if (musicPlayer.RepeatSong) - toSend += "πŸ”‚"; - else if (musicPlayer.RepeatPlaylist) - toSend += "πŸ”"; - toSend += $" `{musicPlayer.Playlist.Count} tracks currently queued. Showing page {page}:` "; - if (musicPlayer.MaxQueueSize != 0 && musicPlayer.Playlist.Count >= musicPlayer.MaxQueueSize) - toSend += "**Song queue is full!**\n"; - else - toSend += "\n"; - const int itemsPerPage = 15; + try { await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); } catch { } + + const int itemsPerPage = 10; int startAt = itemsPerPage * (page - 1); - var number = 1 + startAt; - await channel.SendConfirmAsync(toSend + string.Join("\n", musicPlayer.Playlist.Skip(startAt).Take(15).Select(v => $"`{number++}.` {v.PrettyName}"))).ConfigureAwait(false); + var number = 0 + startAt; + + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName($"Player Queue - Page {page}") + .WithMusicIcon()) + .WithDescription(string.Join("\n", musicPlayer.Playlist + .Skip(startAt) + .Take(10) + .Select(v => $"`{++number}.` {v.PrettyFullName}"))) + .WithFooter(ef => ef.WithText($"{musicPlayer.Playlist.Count} tracks currently queued.")) + .WithOkColor(); + + if (musicPlayer.RepeatSong) + { + embed.WithTitle($"πŸ”‚ Repeating Song: {currentSong.SongInfo.Title} | {currentSong.PrettyFullTime}"); + } + else if (musicPlayer.RepeatPlaylist) + { + embed.WithTitle("πŸ” Repeating Playlist"); + } + if (musicPlayer.MaxQueueSize != 0 && musicPlayer.Playlist.Count >= musicPlayer.MaxQueueSize) + { + embed.WithTitle("🎡 Song queue is full!"); + } + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -197,35 +209,23 @@ namespace NadekoBot.Modules.Music public async Task NowPlaying(IUserMessage umsg) { var channel = (ITextChannel)umsg.Channel; - + MusicPlayer musicPlayer; if (!MusicPlayers.TryGetValue(channel.Guild.Id, out musicPlayer)) return; var currentSong = musicPlayer.CurrentSong; if (currentSong == null) return; - var videoid = Regex.Match(currentSong.SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); + var videoid = Regex.Match(currentSong.SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+"); - if (currentSong.TotalLength == TimeSpan.Zero) - { - await musicPlayer.UpdateSongDurationsAsync().ConfigureAwait(false); - } - var embed = new EmbedBuilder() - .WithAuthor(eab => eab.WithName("Now Playing").WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) - .WithTitle($"{currentSong.SongInfo.Title}") - .WithUrl($"{currentSong.SongInfo.Query}") - .WithDescription($"{currentSong.PrettyCurrentTime()}") - .WithFooter(ef => ef.WithText($"{currentSong.PrettyProvider} | {currentSong.PrettyUser}")) - .WithColor(NadekoBot.OkColor); - if (currentSong.SongInfo.Provider.Equals("YouTube", StringComparison.OrdinalIgnoreCase)) - { - embed.WithThumbnail(tn => tn.Url = $"https://img.youtube.com/vi/{videoid}/0.jpg"); - } - else if (currentSong.SongInfo.Provider.Equals("SoundCloud", StringComparison.OrdinalIgnoreCase)) - { - embed.WithThumbnail(tn => tn.Url = $"{currentSong.SongInfo.AlbumArt}"); - } - await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName("Now Playing") + .WithMusicIcon()) + .WithDescription(currentSong.PrettyName) + .WithFooter(ef => ef.WithText(currentSong.PrettyCurrentTime + "/" + currentSong.PrettyInfo)) + .WithOkColor(); + + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -310,17 +310,25 @@ namespace NadekoBot.Modules.Music } var idArray = ids as string[] ?? ids.ToArray(); var count = idArray.Length; + var msg = await channel.SendMessageAsync($"🎡 Attempting to queue **{count}** songs".SnPl(count) + "...").ConfigureAwait(false); - foreach (var id in idArray) + var cancelSource = new CancellationTokenSource(); + + var tasks = Task.WhenAll(idArray.Select(async id => { + if (cancelSource.Token.IsCancellationRequested) + return; try { await QueueSong(((IGuildUser)umsg.Author), channel, ((IGuildUser)umsg.Author).VoiceChannel, id, true).ConfigureAwait(false); } catch (SongNotFoundException) { } - catch { break; } - } + catch { try { cancelSource.Cancel(); } catch { } } + })); + + await Task.WhenAny(tasks, Task.Delay(Timeout.Infinite, cancelSource.Token)); + await msg.ModifyAsync(m => m.Content = "βœ… Playlist queue complete.").ConfigureAwait(false); } @@ -405,8 +413,7 @@ namespace NadekoBot.Modules.Music await QueueSong(((IGuildUser)umsg.Author), channel, ((IGuildUser)umsg.Author).VoiceChannel, radio_link, musicType: MusicType.Radio).ConfigureAwait(false); if (channel.Guild.GetCurrentUser().GetPermissions(channel).ManageMessages) { - await Task.Delay(10000).ConfigureAwait(false); - await ((IUserMessage)umsg).DeleteAsync().ConfigureAwait(false); + umsg.DeleteAfter(10); } } @@ -453,7 +460,14 @@ namespace NadekoBot.Modules.Music return; var song = (musicPlayer.Playlist as List)?[num - 1]; musicPlayer.RemoveSongAt(num - 1); - await channel.SendConfirmAsync($"🎡 Track {song.PrettyName} at position `#{num}` has been **removed**.").ConfigureAwait(false); + + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName("Song Removed!").WithMusicIcon()) + .AddField(fb => fb.WithName("**Song Position**").WithValue($"#{num}").WithIsInline(true)) + .AddField(fb => fb.WithName("**Song Name**").WithValue($"**[{song.SongInfo.Title.TrimTo(70)}]({song.SongInfo.Query})** `{song.PrettyProvider} | {song.QueuerName.TrimTo(15)}`").WithIsInline(true)) + .WithErrorColor(); + + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } [NadekoCommand, Usage, Description, Aliases] @@ -502,19 +516,15 @@ namespace NadekoBot.Modules.Music 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.SongInfo.Query}") - .WithAuthor(eab => eab.WithName("Song Moved").WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png")) - .AddField(fb => fb.WithName("**From Position**").WithValue($"#{n1}").WithIsInline(true)) - .AddField(fb => fb.WithName("**To Position**").WithValue($"#{n2}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + + var embed = new EmbedBuilder() + .WithTitle($"{s.SongInfo.Title.TrimTo(70)}") + .WithUrl($"{s.SongInfo.Query}") + .WithAuthor(eab => eab.WithName("Song Moved").WithMusicIcon()) + .AddField(fb => fb.WithName("**From Position**").WithValue($"#{n1}").WithIsInline(true)) + .AddField(fb => fb.WithName("**To Position**").WithValue($"#{n2}").WithIsInline(true)) + .WithOkColor(); await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); - - //await channel.SendConfirmAsync($"🎡Moved {s.PrettyName} `from #{n1} to #{n2}`").ConfigureAwait(false); - - } [NadekoCommand, Usage, Description, Aliases] @@ -543,9 +553,15 @@ namespace NadekoBot.Modules.Music if (currentSong == null) return; var currentValue = musicPlayer.ToggleRepeatSong(); - await channel.SendConfirmAsync(currentValue ? - $"πŸ”‚ Repeating track: {currentSong.PrettyName}" : - $"πŸ”‚ Current track repeat stopped.") + + if (currentValue) + await channel.EmbedAsync(new EmbedBuilder() + .WithOkColor() + .WithAuthor(eab => eab.WithMusicIcon().WithName("πŸ”‚ Repeating track")) + .WithDescription(currentSong.PrettyFullName) + .Build()).ConfigureAwait(false); + else + await channel.SendConfirmAsync($"πŸ”‚ Current track repeat stopped.") .ConfigureAwait(false); } @@ -572,7 +588,8 @@ namespace NadekoBot.Modules.Music var curSong = musicPlayer.CurrentSong; var songs = musicPlayer.Playlist.Append(curSong) - .Select(s=> new PlaylistSong() { + .Select(s => new PlaylistSong() + { Provider = s.SongInfo.Provider, ProviderType = s.SongInfo.ProviderType, Title = s.SongInfo.Title, @@ -646,9 +663,12 @@ namespace NadekoBot.Modules.Music playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num); } - await channel.SendConfirmAsync($@"🎢 **Page {num} of saved playlists:** + var embed = new EmbedBuilder() + .WithAuthor(eab => eab.WithName($"Page {num} of Saved Playlists").WithMusicIcon()) + .WithDescription(string.Join("\n", playlists.Select(r => $"`#{r.Id}` - **{r.Name}**\t by **`{r.Author}`**\t ({r.Songs.Count} songs)"))) + .WithOkColor(); + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); -" + string.Join("\n", playlists.Select(r => $"`#{r.Id}` - **{r.Name}** by __{r.Author}__ ({r.Songs.Count} songs)"))).ConfigureAwait(false); } //todo only author or owner @@ -750,7 +770,7 @@ namespace NadekoBot.Modules.Music } else { - await channel.SendConfirmAsync($"🎢 Selected song **{selSong.SongInfo.Title}**: <{selSong.SongInfo.Query}>").ConfigureAwait(false); + await channel.SendMessageAsync($"🎢 Selected song **{selSong.SongInfo.Title}**: <{selSong.SongInfo.Query}>").ConfigureAwait(false); } } else @@ -758,7 +778,7 @@ namespace NadekoBot.Modules.Music var curSong = musicPlayer.CurrentSong; if (curSong == null) return; - await channel.SendConfirmAsync($"🎢 Current song **{curSong.SongInfo.Title}**: <{curSong.SongInfo.Query}>").ConfigureAwait(false); + await channel.SendMessageAsync($"🎢 Current song **{curSong.SongInfo.Title}**: <{curSong.SongInfo.Query}>").ConfigureAwait(false); } } @@ -796,47 +816,61 @@ namespace NadekoBot.Modules.Music vol = uow.GuildConfigs.For(textCh.Guild.Id, set => set).DefaultMusicVolume; } var mp = new MusicPlayer(voiceCh, vol); - IUserMessage playingMessage = null; - IUserMessage lastFinishedMessage = null; - mp.OnCompleted += async (s, song) => - { - if (song.PrintStatusMessage) - { - try - { - if (lastFinishedMessage != null) - await lastFinishedMessage.DeleteAsync().ConfigureAwait(false); - if (playingMessage != null) - await playingMessage.DeleteAsync().ConfigureAwait(false); - try { lastFinishedMessage = await textCh.SendConfirmAsync($"🎡 Finished {song.PrettyName}").ConfigureAwait(false); } catch { } - if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.Provider == "YouTube") - { - await QueueSong(queuer.Guild.GetCurrentUser(), textCh, voiceCh, (await NadekoBot.Google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList().Shuffle().FirstOrDefault(), silent, musicType).ConfigureAwait(false); - } - } - catch { } - } - }; - mp.OnStarted += async (s, song) => - { - if (song.PrintStatusMessage) - { - var sender = s as MusicPlayer; - if (sender == null) - return; - var msgTxt = $"🎡 Playing {song.PrettyName}\t `Vol: {(int)(sender.Volume * 100)}%`"; - try { playingMessage = await textCh.SendConfirmAsync(msgTxt).ConfigureAwait(false); } catch { } - } - }; - mp.OnPauseChanged += async (paused) => + IUserMessage finishedMessage = null; + mp.OnCompleted += async (s, song) => { try { + if (finishedMessage != null) + finishedMessage.DeleteAfter(0); + finishedMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName("Finished Song").WithMusicIcon()) + .WithDescription(song.PrettyName) + .WithFooter(ef => ef.WithText(song.PrettyInfo)) + .Build()) + .ConfigureAwait(false); + + if (mp.Autoplay && mp.Playlist.Count == 0 && song.SongInfo.Provider == "YouTube") + { + await QueueSong(queuer.Guild.GetCurrentUser(), textCh, voiceCh, (await NadekoBot.Google.GetRelatedVideosAsync(song.SongInfo.Query, 4)).ToList().Shuffle().FirstOrDefault(), silent, musicType).ConfigureAwait(false); + } + } + catch { } + }; + IUserMessage playingMessage = null; + mp.OnStarted += async (player, song) => + { + try + { + if (playingMessage != null) + playingMessage.DeleteAfter(0); + + playingMessage = await textCh.EmbedAsync(new EmbedBuilder().WithOkColor() + .WithAuthor(eab => eab.WithName("Playing Song").WithMusicIcon()) + .WithDescription(song.PrettyName) + .WithFooter(ef => ef.WithText(song.PrettyInfo)) + .Build()) + .ConfigureAwait(false); + } + catch { } + }; + + mp.OnPauseChanged += async (paused) => + { + try + { + IUserMessage pauseMessage = null; if (paused) - await textCh.SendConfirmAsync("🎡 Music playback **paused**.").ConfigureAwait(false); + { + pauseMessage = await textCh.SendConfirmAsync("🎡 Music playback **paused**.").ConfigureAwait(false); + } else - await textCh.SendConfirmAsync("🎡 Music playback **resumed**.").ConfigureAwait(false); + { + pauseMessage = await textCh.SendConfirmAsync("🎡 Music playback **resumed**.").ConfigureAwait(false); + } + if (pauseMessage != null) + pauseMessage.DeleteAfter(15); } catch { } }; @@ -846,7 +880,7 @@ namespace NadekoBot.Modules.Music try { musicPlayer.ThrowIfQueueFull(); - resolvedSong = await Song.ResolveSong(query, musicType).ConfigureAwait(false); + resolvedSong = await SongHandler.ResolveSong(query, musicType).ConfigureAwait(false); if (resolvedSong == null) throw new SongNotFoundException(); @@ -862,20 +896,19 @@ namespace NadekoBot.Modules.Music { try { - var queuedMessage = await textCh.SendConfirmAsync($"🎡 Queued **{resolvedSong.SongInfo.Title.TrimTo(55)}** at `#{musicPlayer.Playlist.Count + 1}`").ConfigureAwait(false); - var t = Task.Run(async () => - { - try - { - await Task.Delay(10000).ConfigureAwait(false); - - await queuedMessage.DeleteAsync().ConfigureAwait(false); - } - catch { } - }).ConfigureAwait(false); + //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("Queued Song").WithMusicIcon()) + .WithTitle($"{resolvedSong.SongInfo.Title}") + .WithDescription($"Queue #{musicPlayer.Playlist.Count + 1}") + .WithFooter(ef => ef.WithText($"{resolvedSong.PrettyProvider}")) + .Build()) + .ConfigureAwait(false); + if (queuedMessage != null) + queuedMessage.DeleteAfter(10); } catch { } // if queued message sending fails, don't attempt to delete it } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/NSFW/NSFW.cs b/src/NadekoBot/Modules/NSFW/NSFW.cs index 0fba9e0e..62603807 100644 --- a/src/NadekoBot/Modules/NSFW/NSFW.cs +++ b/src/NadekoBot/Modules/NSFW/NSFW.cs @@ -247,7 +247,7 @@ namespace NadekoBot.Modules.NSFW if (matches.Count == 0) return null; - return matches[rng.Next(0, matches.Count)].Groups["ll"].Value; + return "http:" + matches[rng.Next(0, matches.Count)].Groups["ll"].Value; } } diff --git a/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs b/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs index 2d90a2e7..af856f9b 100644 --- a/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs +++ b/src/NadekoBot/Modules/Permissions/Commands/BlacklistCommands.cs @@ -105,7 +105,10 @@ namespace NadekoBot.Modules.Permissions } - await channel.SendConfirmAsync($"Blacklisted a `{type}` with id `{id}`").ConfigureAwait(false); + if(action == AddRemove.Add) + await channel.SendConfirmAsync($"Blacklisted a `{type}` with id `{id}`").ConfigureAwait(false); + else + await channel.SendConfirmAsync($"Unblacklisted a `{type}` with id `{id}`").ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Pokemon/Pokemon.cs b/src/NadekoBot/Modules/Pokemon/Pokemon.cs index 31dbf2cb..0861bc1d 100644 --- a/src/NadekoBot/Modules/Pokemon/Pokemon.cs +++ b/src/NadekoBot/Modules/Pokemon/Pokemon.cs @@ -316,7 +316,7 @@ namespace NadekoBot.Modules.Pokemon var targetType = StringToPokemonType(typeTargeted); if (targetType == null) { - await channel.EmbedAsync(PokemonTypes.Aggregate(new EmbedBuilder().WithDescription("List of the available types:"), (eb, pt) => eb.AddField(efb => efb.WithName(pt.Name).WithValue(pt.Icon).WithIsInline(true))).WithColor(NadekoBot.OkColor).Build()).ConfigureAwait(false); + await channel.EmbedAsync(PokemonTypes.Aggregate(new EmbedBuilder().WithDescription("List of the available types:"), (eb, pt) => eb.AddField(efb => efb.WithName(pt.Name).WithValue(pt.Icon).WithIsInline(true))).WithOkColor().Build()).ConfigureAwait(false); return; } if (targetType == GetPokeType(user.Id)) diff --git a/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs b/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs index b5327810..3852786e 100644 --- a/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/JokeCommands.cs @@ -49,6 +49,7 @@ namespace NadekoBot.Modules.Searches using (var http = new HttpClient()) { var response = await http.GetStringAsync("http://api.yomomma.info/").ConfigureAwait(false); + System.Console.WriteLine(response); await msg.Channel.SendConfirmAsync(JObject.Parse(response)["joke"].ToString() + " πŸ˜†").ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs index e50f2060..25c18267 100644 --- a/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/LoLCommands.cs @@ -51,7 +51,7 @@ namespace NadekoBot.Modules.Searches $"limit={showCount}") .ConfigureAwait(false))["data"] as JArray; var dataList = data.Distinct(new ChampionNameComparer()).Take(showCount).ToList(); - var eb = new EmbedBuilder().WithColor(NadekoBot.OkColor).WithTitle(Format.Underline($"{dataList.Count} most banned champions")); + var eb = new EmbedBuilder().WithOkColor().WithTitle(Format.Underline($"{dataList.Count} most banned champions")); for (var i = 0; i < dataList.Count; i++) { var champ = dataList[i]; diff --git a/src/NadekoBot/Modules/Searches/Commands/MemegenCommands.cs b/src/NadekoBot/Modules/Searches/Commands/MemegenCommands.cs index 3c48eab0..541eac15 100644 --- a/src/NadekoBot/Modules/Searches/Commands/MemegenCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/MemegenCommands.cs @@ -39,8 +39,8 @@ namespace NadekoBot.Modules.Searches { var channel = (ITextChannel)umsg.Channel; - var top = Uri.EscapeDataString(topText.Replace(' ', '-')); - var bot = Uri.EscapeDataString(botText.Replace(' ', '-')); + var top = topText.Replace(' ', '-'); + var bot = botText.Replace(' ', '-'); await channel.SendMessageAsync($"http://memegen.link/{meme}/{top}/{bot}.jpg") .ConfigureAwait(false); } diff --git a/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs b/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs new file mode 100644 index 00000000..d3c5742f --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Commands/Models/WeatherModels.cs @@ -0,0 +1,67 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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 int pressure { get; set; } + public int 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/Modules/Searches/Commands/OMDB/OmdbProvider.cs b/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs index 02373bb3..b73973a2 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs +++ b/src/NadekoBot/Modules/Searches/Commands/OMDB/OmdbProvider.cs @@ -1,5 +1,6 @@ ο»Ώusing Discord; using Discord.API; +using NadekoBot.Extensions; using Newtonsoft.Json; using System; using System.Net.Http; @@ -35,7 +36,7 @@ namespace NadekoBot.Modules.Searches.Commands.OMDB public string Poster { get; set; } public Embed GetEmbed() => - new EmbedBuilder().WithColor(NadekoBot.OkColor) + new EmbedBuilder().WithOkColor() .WithTitle(Title) .WithUrl($"http://www.imdb.com/title/{ImdbId}/") .WithDescription(Plot) diff --git a/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs b/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs index ec3ddfb1..3500ab83 100644 --- a/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/OverwatchCommands.cs @@ -31,12 +31,15 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(query)) return; var battletag = Regex.Replace(query, "#", "-", RegexOptions.IgnoreCase); + + await channel.TriggerTypingAsync().ConfigureAwait(false); try { var model = await GetProfile(region, battletag); var rankimg = $"{model.Competitive.rank_img}"; var rank = $"{model.Competitive.rank}"; + var competitiveplay = $"{model.Games.Competitive.played}"; if (string.IsNullOrWhiteSpace(rank)) { var embed = new EmbedBuilder() @@ -52,7 +55,7 @@ namespace NadekoBot.Modules.Searches .AddField(fb => fb.WithName("**Competitive Rank**").WithValue("0").WithIsInline(true)) .AddField(fb => fb.WithName("**Competitive Playtime**").WithValue($"{model.Playtime.competitive}").WithIsInline(true)) .AddField(fb => fb.WithName("**Quick Playtime**").WithValue($"{model.Playtime.quick}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + .WithOkColor(); await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } else @@ -70,9 +73,22 @@ namespace NadekoBot.Modules.Searches .AddField(fb => fb.WithName("**Competitive Rank**").WithValue(rank).WithIsInline(true)) .AddField(fb => fb.WithName("**Competitive Playtime**").WithValue($"{model.Playtime.competitive}").WithIsInline(true)) .AddField(fb => fb.WithName("**Quick Playtime**").WithValue($"{model.Playtime.quick}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + .WithOkColor(); + await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); + } + if (string.IsNullOrWhiteSpace(competitiveplay)) + { + var embed = new EmbedBuilder() + .WithAuthor(eau => eau.WithName($"{model.username}") + .WithUrl($"https://www.overbuff.com/players/pc/{battletag}") + .WithIconUrl($"{model.avatar}")) + .WithThumbnail(th => th.WithUrl("https://cdn.discordapp.com/attachments/155726317222887425/255653487512256512/YZ4w2ey.png")) + .AddField(fb => fb.WithName("**Level**").WithValue($"{model.level}").WithIsInline(true)) + .AddField(fb => fb.WithName("**Quick Wins**").WithValue($"{model.Games.Quick.wins}").WithIsInline(true)) + .AddField(fb => fb.WithName("**Competitive Playtime**").WithValue($"0 hour").WithIsInline(true)) + .AddField(fb => fb.WithName("**Quick Playtime**").WithValue($"{model.Playtime.quick}").WithIsInline(true)) + .WithOkColor(); await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); - return; } } catch @@ -80,23 +96,23 @@ namespace NadekoBot.Modules.Searches await channel.SendErrorAsync("Found no user! Please check the **Region** and **BattleTag** before trying again."); } } - public async Task GetProfile(string region, string battletag) - { - try - { - using (var http = new HttpClient()) - { - var Url = await http.GetStringAsync($"https://api.lootbox.eu/pc/{region.ToLower()}/{battletag}/profile"); - var model = JsonConvert.DeserializeObject(Url); - return model.data; - } - } - catch - { - return null; - } - } - - } + public async Task GetProfile(string region, string battletag) + { + try + { + using (var http = new HttpClient()) + { + var Url = await http.GetStringAsync($"https://api.lootbox.eu/pc/{region.ToLower()}/{battletag}/profile"); + var model = JsonConvert.DeserializeObject(Url); + return model.data; + } + } + catch + { + return null; + } + } + + } } } diff --git a/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs b/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs index 60b9c27e..36ead93d 100644 --- a/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/PokemonSearchCommands.cs @@ -55,7 +55,7 @@ namespace NadekoBot.Modules.Searches if (kvp.Key.ToUpperInvariant() == pokemon.ToUpperInvariant()) { var p = kvp.Value; - await channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithTitle(kvp.Key.ToTitleCase()) .WithDescription(p.BaseStats.ToString()) .AddField(efb => efb.WithName("Types").WithValue(string.Join(",\n", p.Types)).WithIsInline(true)) @@ -81,7 +81,7 @@ namespace NadekoBot.Modules.Searches { if (kvp.Key.ToUpperInvariant() == ability) { - await channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithTitle(kvp.Value.Name) .WithDescription(kvp.Value.Desc) .AddField(efb => efb.WithName("Rating").WithValue(kvp.Value.Rating.ToString()).WithIsInline(true)) diff --git a/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs b/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs index f5700698..49478c2c 100644 --- a/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs +++ b/src/NadekoBot/Modules/Searches/Commands/XkcdCommands.cs @@ -56,7 +56,7 @@ namespace NadekoBot.Modules.Searches var res = await http.GetStringAsync($"{xkcdUrl}/{num}/info.0.json").ConfigureAwait(false); var comic = JsonConvert.DeserializeObject(res); - var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor) + var embed = new EmbedBuilder().WithOkColor() .WithImage(eib => eib.WithUrl(comic.ImageLink)) .WithAuthor(eab => eab.WithName(comic.Title).WithUrl($"{xkcdUrl}/{num}").WithIconUrl("http://xkcd.com/s/919f27.ico")) .AddField(efb => efb.WithName("Comic#").WithValue(comic.Num.ToString()).WithIsInline(true)) diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index a3f5eb0e..6c05e4c5 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -17,6 +17,7 @@ using ImageSharp; using NadekoBot.Extensions; using System.IO; using NadekoBot.Modules.Searches.Commands.OMDB; +using NadekoBot.Modules.Searches.Commands.Models; namespace NadekoBot.Modules.Searches { @@ -25,28 +26,30 @@ namespace NadekoBot.Modules.Searches { [NadekoCommand, Usage, Description, Aliases] [RequireContext(ContextType.Guild)] - public async Task Weather(IUserMessage umsg, string city, string country) + public async Task Weather(IUserMessage umsg, [Remainder] string query) { var channel = (ITextChannel)umsg.Channel; - city = city.Replace(" ", ""); - country = city.Replace(" ", ""); + if (string.IsNullOrWhiteSpace(query)) + return; + string response; using (var http = new HttpClient()) - response = await http.GetStringAsync($"http://api.ninetales.us/nadekobot/weather/?city={city}&country={country}").ConfigureAwait(false); + response = await http.GetStringAsync($"http://api.openweathermap.org/data/2.5/weather?q={query}&appid=42cd627dd60debf25a5739e50a217d74&units=metric").ConfigureAwait(false); - var obj = JObject.Parse(response)["weather"]; + var data = JsonConvert.DeserializeObject(response); var embed = new EmbedBuilder() - .AddField(fb => fb.WithName("🌍 **Location**").WithValue($"{obj["target"]}").WithIsInline(true)) - .AddField(fb => fb.WithName("πŸ“ **Lat,Long**").WithValue($"{obj["latitude"]}, {obj["longitude"]}").WithIsInline(true)) - .AddField(fb => fb.WithName("☁ **Condition**").WithValue($"{obj["condition"]}").WithIsInline(true)) - .AddField(fb => fb.WithName("πŸ˜“ **Humidity**").WithValue($"{obj["humidity"]}%").WithIsInline(true)) - .AddField(fb => fb.WithName("πŸ’¨ **Wind Speed**").WithValue($"{obj["windspeedk"]}km/h ({obj["windspeedm"]}mph)").WithIsInline(true)) - .AddField(fb => fb.WithName("🌑 **Temperature**").WithValue($"{obj["centigrade"]}Β°C ({obj["fahrenheit"]}Β°F)").WithIsInline(true)) - .AddField(fb => fb.WithName("πŸ”† **Feels like**").WithValue($"{obj["feelscentigrade"]}Β°C ({obj["feelsfahrenheit"]}Β°F)").WithIsInline(true)) - .AddField(fb => fb.WithName("πŸŒ„ **Sunrise**").WithValue($"{obj["sunrise"]}").WithIsInline(true)) - .AddField(fb => fb.WithName("πŸŒ‡ **Sunset**").WithValue($"{obj["sunset"]}").WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + .AddField(fb => fb.WithName("🌍 **Location**").WithValue(data.name + ", " + data.sys.country).WithIsInline(true)) + .AddField(fb => fb.WithName("πŸ“ **Lat,Long**").WithValue($"{data.coord.lat}, {data.coord.lon}").WithIsInline(true)) + .AddField(fb => fb.WithName("☁ **Condition**").WithValue(String.Join(", ", data.weather.Select(w=>w.main))).WithIsInline(true)) + .AddField(fb => fb.WithName("πŸ˜“ **Humidity**").WithValue($"{data.main.humidity}%").WithIsInline(true)) + .AddField(fb => fb.WithName("πŸ’¨ **Wind Speed**").WithValue(data.wind.speed + " km/h").WithIsInline(true)) + .AddField(fb => fb.WithName("🌑 **Temperature**").WithValue(data.main.temp + "Β°C").WithIsInline(true)) + .AddField(fb => fb.WithName("πŸ”† **Min - Max**").WithValue($"{data.main.temp_min}Β°C - {data.main.temp_max}Β°C").WithIsInline(true)) + .AddField(fb => fb.WithName("πŸŒ„ **Sunrise (utc)**").WithValue($"{data.sys.sunrise.ToUnixTimestamp():HH:mm}").WithIsInline(true)) + .AddField(fb => fb.WithName("πŸŒ‡ **Sunset (utc)**").WithValue($"{data.sys.sunset.ToUnixTimestamp():HH:mm}").WithIsInline(true)) + .WithOkColor() + .WithFooter(efb => efb.WithText("Powered by http://openweathermap.org")); await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } @@ -202,7 +205,7 @@ namespace NadekoBot.Modules.Searches await msg.Channel.SendErrorAsync("Failed to shorten that url.").ConfigureAwait(false); } - await msg.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await msg.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() .AddField(efb => efb.WithName("Original Url") .WithValue($"<{arg}>")) .AddField(efb => efb.WithName("Short Url") @@ -221,7 +224,7 @@ namespace NadekoBot.Modules.Searches if (string.IsNullOrWhiteSpace(terms)) return; - await channel.SendConfirmAsync($"https://google.com/search?q={ WebUtility.UrlEncode(terms).Replace(' ', '+') }") + await channel.SendMessageAsync($"https://google.com/search?q={ WebUtility.UrlEncode(terms).Replace(' ', '+') }") .ConfigureAwait(false); } @@ -255,7 +258,7 @@ namespace NadekoBot.Modules.Searches var desc = item["text"].ToString(); var types = String.Join(",\n", item["types"].ToObject()); var img = item["editions"][0]["image_url"].ToString(); - var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor) + var embed = new EmbedBuilder().WithOkColor() .WithTitle(item["name"].ToString()) .WithDescription(desc) .WithImage(eib => eib.WithUrl(img)) @@ -364,7 +367,7 @@ namespace NadekoBot.Modules.Searches .WithUrl("http://www.yodaspeak.co.uk/") .WithAuthor(au => au.WithName("Yoda").WithIconUrl("http://www.yodaspeak.co.uk/yoda-small1.gif")) .WithDescription(res) - .WithColor(NadekoBot.OkColor); + .WithOkColor(); await channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } catch @@ -405,7 +408,7 @@ namespace NadekoBot.Modules.Searches var word = item["word"].ToString(); var def = item["definition"].ToString(); var link = item["permalink"].ToString(); - var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor) + var embed = new EmbedBuilder().WithOkColor() .WithUrl(link) .WithAuthor(eab => eab.WithIconUrl("http://i.imgur.com/nwERwQE.jpg").WithName(word)) .WithDescription(def); @@ -452,7 +455,7 @@ namespace NadekoBot.Modules.Searches var hashtag = item["hashtag"].ToString(); var link = item["uri"].ToString(); var desc = item["text"].ToString(); - await channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor) + await channel.EmbedAsync(new EmbedBuilder().WithOkColor() .WithAuthor(eab => eab.WithUrl(link) .WithIconUrl("http://res.cloudinary.com/urbandictionary/image/upload/a_exif,c_fit,h_200,w_200/v1394975045/b8oszuu3tbq7ebyo7vo1.jpg") .WithName(query)) diff --git a/src/NadekoBot/Modules/Utility/Commands/CalcCommand.cs b/src/NadekoBot/Modules/Utility/Commands/CalcCommand.cs index 31a91f6f..120fc4fc 100644 --- a/src/NadekoBot/Modules/Utility/Commands/CalcCommand.cs +++ b/src/NadekoBot/Modules/Utility/Commands/CalcCommand.cs @@ -49,7 +49,7 @@ namespace NadekoBot.Modules.Utility "Equals", "GetHashCode", "GetType"}); - await msg.Channel.SendConfirmAsync(string.Join(", ",selection)); + await msg.Channel.SendConfirmAsync("Available functions in calc", string.Join(", ", selection)); } } diff --git a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs index 740b18e0..e5ecdb10 100644 --- a/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/InfoCommands.cs @@ -44,7 +44,7 @@ namespace NadekoBot.Modules.Utility .AddField(fb => fb.WithName("**Region**").WithValue(guild.VoiceRegionId.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName("**Roles**").WithValue(guild.Roles.Count().ToString()).WithIsInline(true)) .WithImage(tn => tn.WithUrl(guild.IconUrl)) - .WithColor(NadekoBot.OkColor); + .WithOkColor(); if (guild.Emojis.Count() > 0) { embed.AddField(fb => fb.WithName("**Custom Emojis**").WithValue(Format.Italics(string.Join(", ", guild.Emojis))).WithIsInline(true)); @@ -67,7 +67,7 @@ namespace NadekoBot.Modules.Utility .AddField(fb => fb.WithName("**ID**").WithValue(ch.Id.ToString()).WithIsInline(true)) .AddField(fb => fb.WithName("**Created At**").WithValue($"{createdAt.ToString("dd.MM.yyyy HH:mm")}").WithIsInline(true)) .AddField(fb => fb.WithName("**Users**").WithValue(usercount.ToString()).WithIsInline(true)) - .WithColor(NadekoBot.OkColor); + .WithOkColor(); await msg.Channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } @@ -92,7 +92,7 @@ namespace NadekoBot.Modules.Utility .AddField(fb => fb.WithName("**Current Game**").WithValue($"{(user.Game?.Name == null ? "-" : user.Game.Name)}").WithIsInline(true)) .AddField(fb => fb.WithName("**Roles**").WithValue($"**({user.Roles.Count()})** - {string.Join(", ", user.Roles.Select(r => r.Name)).SanitizeMentions()}").WithIsInline(true)) .WithThumbnail(tn => tn.WithUrl(user.AvatarUrl)) - .WithColor(NadekoBot.OkColor); + .WithOkColor(); await msg.Channel.EmbedAsync(embed.Build()).ConfigureAwait(false); } } diff --git a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs index 3468a15f..78874828 100644 --- a/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs +++ b/src/NadekoBot/Modules/Utility/Commands/UnitConversion.cs @@ -106,7 +106,7 @@ namespace NadekoBot.Modules.Utility { var res = Units.GroupBy(x => x.UnitType) .Aggregate(new EmbedBuilder().WithTitle("__Units which can be used by the converter__") - .WithColor(NadekoBot.OkColor), + .WithOkColor(), (embed, g) => embed.AddField(efb => efb.WithName(g.Key.ToTitleCase()) .WithValue(String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 1e6fce9c..e4e2fe31 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -35,16 +35,16 @@ namespace NadekoBot.Modules.Utility game = game.Trim().ToUpperInvariant(); if (string.IsNullOrWhiteSpace(game)) return; - var arr = (await (umsg.Channel as IGuildChannel).Guild.GetUsersAsync()) + var usrs = (await (umsg.Channel as IGuildChannel).Guild.GetUsersAsync()) .Where(u => u.Game?.Name?.ToUpperInvariant() == game) .Select(u => u.Username) .ToList(); int i = 0; - if (!arr.Any()) + if (!usrs.Any()) await channel.SendErrorAsync("Nobody is playing that game.").ConfigureAwait(false); else - await channel.SendConfirmAsync("```css\n" + string.Join("\n", arr.GroupBy(item => (i++) / 2) + await channel.SendConfirmAsync($"List of users playing {game} game. Total {usrs.Count}.", "```css\n" + string.Join("\n", usrs.Take(30).GroupBy(item => (i++) / 2) .Select(ig => string.Concat(ig.Select(el => $"β€’ {el,-27}")))) + "\n```") .ConfigureAwait(false); } @@ -132,13 +132,30 @@ namespace NadekoBot.Modules.Utility if (page < 1 || page > 100) return; + if (target != null) { - await channel.SendConfirmAsync($"βš” **Page #{page} of roles for {target.Username}**", $"```css\nβ€’ " + string.Join("\nβ€’ ", target.Roles.Except(new[] { guild.EveryoneRole }).OrderBy(r => -r.Position).Skip((page - 1) * RolesPerPage).Take(RolesPerPage)).SanitizeMentions() + "\n```"); + var roles = target.Roles.Except(new[] { guild.EveryoneRole }).OrderBy(r => -r.Position).Skip((page - 1) * RolesPerPage).Take(RolesPerPage); + if (!roles.Any()) + { + await channel.SendErrorAsync("No roles on this page.").ConfigureAwait(false); + } + else + { + await channel.SendConfirmAsync($"βš” **Page #{page} of roles for {target.Username}**", $"```css\nβ€’ " + string.Join("\nβ€’ ", roles).SanitizeMentions() + "\n```").ConfigureAwait(false); + } } else { - await channel.SendConfirmAsync($"βš” **Page #{page} of all roles on this server:**", $"```css\nβ€’ " + string.Join("\nβ€’ ", guild.Roles.Except(new[] { guild.EveryoneRole }).OrderBy(r => -r.Position).Skip((page - 1) * RolesPerPage).Take(RolesPerPage)).SanitizeMentions() + "\n```"); + var roles = guild.Roles.Except(new[] { guild.EveryoneRole }).OrderBy(r => -r.Position).Skip((page - 1) * RolesPerPage).Take(RolesPerPage); + if (!roles.Any()) + { + await channel.SendErrorAsync("No roles on this page.").ConfigureAwait(false); + } + else + { + await channel.SendConfirmAsync($"βš” **Page #{page} of all roles on this server:**", $"```css\nβ€’ " + string.Join("\nβ€’ ", roles).SanitizeMentions() + "\n```").ConfigureAwait(false); + } } } @@ -263,7 +280,7 @@ namespace NadekoBot.Modules.Utility return; } - await channel.EmbedAsync(guilds.Aggregate(new EmbedBuilder().WithColor(NadekoBot.OkColor), + await channel.EmbedAsync(guilds.Aggregate(new EmbedBuilder().WithOkColor(), (embed, g) => embed.AddField(efb => efb.WithName(g.Name) .WithValue($"```css\nID: {g.Id}\nMembers: {g.GetUsers().Count}\nOwnerID: {g.OwnerId} ```") .WithIsInline(false))) @@ -271,4 +288,4 @@ namespace NadekoBot.Modules.Utility .ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Resources/CommandStrings.Designer.cs b/src/NadekoBot/Resources/CommandStrings.Designer.cs index 2728e701..d4e2eeda 100644 --- a/src/NadekoBot/Resources/CommandStrings.Designer.cs +++ b/src/NadekoBot/Resources/CommandStrings.Designer.cs @@ -86,6 +86,33 @@ namespace NadekoBot.Resources { } } + /// + /// Looks up a localized string similar to acrophobia acro. + /// + public static string acro_cmd { + get { + return ResourceManager.GetString("acro_cmd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Starts an Acrophobia game. Second argment is optional round length in seconds. (default is 60). + /// + public static string acro_desc { + get { + return ResourceManager.GetString("acro_desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to `{0}acro` or `{0}acro 30`. + /// + public static string acro_usage { + get { + return ResourceManager.GetString("acro_usage", resourceCulture); + } + } + /// /// Looks up a localized string similar to addcustreact acr. /// @@ -4648,8 +4675,8 @@ namespace NadekoBot.Resources { return ResourceManager.GetString("osub_usage", resourceCulture); } } - - /// + + /// /// Looks up a localized string similar to overwatch ow. /// public static string overwatch_cmd { @@ -7818,7 +7845,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to Shows weather data for a specified city and a country. BOTH ARE REQUIRED. Use country abbrevations.. + /// Looks up a localized string similar to Shows weather data for a specified city. You can also specify a country after a comma.. /// public static string weather_desc { get { @@ -7827,7 +7854,7 @@ namespace NadekoBot.Resources { } /// - /// Looks up a localized string similar to `{0}we Moscow RF`. + /// Looks up a localized string similar to `{0}we Moscow, RU`. /// public static string weather_usage { get { diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index 3e6df293..0b6c7881 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1876,10 +1876,10 @@ weather we - Shows weather data for a specified city and a country. BOTH ARE REQUIRED. Use country abbrevations. + Shows weather data for a specified city. You can also specify a country after a comma. - `{0}we Moscow RF` + `{0}we Moscow, RU` youtube yt @@ -2781,7 +2781,7 @@ `{0}crstats` or `{0}crstats 3` - + overwatch ow @@ -2790,4 +2790,13 @@ `{0}ow us Battletag#1337` or `{0}overwatch eu Battletag#2016` + + acrophobia acro + + + Starts an Acrophobia game. Second argment is optional round length in seconds. (default is 60) + + + `{0}acro` or `{0}acro 30` + \ No newline at end of file diff --git a/src/NadekoBot/Services/Impl/GoogleApiService.cs b/src/NadekoBot/Services/Impl/GoogleApiService.cs index 6a18c686..6e5727f7 100644 --- a/src/NadekoBot/Services/Impl/GoogleApiService.cs +++ b/src/NadekoBot/Services/Impl/GoogleApiService.cs @@ -167,7 +167,8 @@ namespace NadekoBot.Services.Impl remaining -= toGet; var q = yt.Videos.List("contentDetails"); - q.Id = string.Join(",", videoIds); + q.Id = string.Join(",", videoIdsList.Take(toGet)); + videoIdsList = videoIdsList.Skip(toGet).ToList(); var items = (await q.ExecuteAsync().ConfigureAwait(false)).Items; foreach (var i in items) { diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index a94681bb..4c98b5dd 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -25,6 +25,23 @@ namespace NadekoBot.Extensions http.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); } + 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 () => + { + await Task.Delay(seconds * 1000); + try { await msg.DeleteAsync().ConfigureAwait(false); } + catch { } + }); + return msg; + } + public static async Task SendMessageToOwnerAsync(this IGuild guild, string message) { var ownerPrivate = await (await guild.GetOwnerAsync().ConfigureAwait(false)).CreateDMChannelAsync() @@ -65,6 +82,8 @@ namespace NadekoBot.Extensions public static double UnixTimestamp(this DateTime dt) => dt.ToUniversalTime().Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + public static DateTime ToUnixTimestamp(this double number) => new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(number); + public static async Task SendMessageAsync(this IGuildUser user, string message, bool isTTS = false) => await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(message, isTTS).ConfigureAwait(false); diff --git a/src/NadekoBot/project.json b/src/NadekoBot/project.json index cbf49aa2..4aeef449 100644 --- a/src/NadekoBot/project.json +++ b/src/NadekoBot/project.json @@ -26,7 +26,7 @@ "target": "project" }, "Google.Apis.Urlshortener.v1": "1.19.0.138", - "Google.Apis.YouTube.v3": "1.19.0.655", + "Google.Apis.YouTube.v3": "1.20.0.701", "ImageSharp": "1.0.0-alpha-000079", "Microsoft.EntityFrameworkCore": "1.1.0", "Microsoft.EntityFrameworkCore.Design": "1.1.0",