diff --git a/CHANGELOG.md b/CHANGELOG.md index aac19e806..14719c74e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog. ### Added - `.remindl` and `.remindrm` commands now supports optional 'server' parameter for Administrators which allows them to delete any reminder created on the server - Added slots.currencyFontColor to gambling.yml +- Added `.qexport` and `.qimport` commands which allow you to export and import quotes just like `.crsexport` ### Fixed - `.crypto` now supports top 5000 coins diff --git a/src/NadekoBot/Db/Extensions/QuoteExtensions.cs b/src/NadekoBot/Db/Extensions/QuoteExtensions.cs index bd1841ba1..cdcb56d83 100644 --- a/src/NadekoBot/Db/Extensions/QuoteExtensions.cs +++ b/src/NadekoBot/Db/Extensions/QuoteExtensions.cs @@ -9,6 +9,11 @@ namespace NadekoBot.Db { public static class QuoteExtensions { + public static IEnumerable GetForGuild(this DbSet quotes, ulong guildId) + { + return quotes.AsQueryable().Where(x => x.GuildId == guildId); + } + public static IEnumerable GetGroup(this DbSet quotes, ulong guildId, int page, OrderType order) { var q = quotes.AsQueryable().Where(x => x.GuildId == guildId); diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index dfda73b75..af194db03 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -310,7 +310,7 @@ namespace NadekoBot.Modules.CustomReactions _ = ctx.Channel.TriggerTypingAsync(); var serialized = _service.ExportCrs(ctx.Guild?.Id); - using var stream = await serialized.ToStream(); + await using var stream = await serialized.ToStream(); await ctx.Channel.SendFileAsync(stream, "crs-export.yml", text: null); } diff --git a/src/NadekoBot/Modules/Utility/QuoteCommands.cs b/src/NadekoBot/Modules/Utility/QuoteCommands.cs index 2e256be2a..475cd6eb8 100644 --- a/src/NadekoBot/Modules/Utility/QuoteCommands.cs +++ b/src/NadekoBot/Modules/Utility/QuoteCommands.cs @@ -7,10 +7,13 @@ using NadekoBot.Db.Models; using NadekoBot.Extensions; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; +using NadekoBot.Common.Yml; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; +using YamlDotNet.Serialization; namespace NadekoBot.Modules.Utility { @@ -20,10 +23,12 @@ namespace NadekoBot.Modules.Utility public class QuoteCommands : NadekoSubmodule { private readonly DbService _db; + private readonly IHttpClientFactory _http; - public QuoteCommands(DbService db) + public QuoteCommands(DbService db, IHttpClientFactory http) { _db = db; + _http = http; } [NadekoCommand, Aliases] @@ -248,6 +253,140 @@ namespace NadekoBot.Modules.Utility await ReplyConfirmLocalizedAsync(strs.quotes_deleted(Format.Bold(keyword.SanitizeAllMentions()))).ConfigureAwait(false); } + + public class ExportedQuote + { + public static ExportedQuote FromModel(Quote quote) + => new ExportedQuote() + { + Id = ((kwum)quote.Id).ToString(), + An = quote.AuthorName, + Aid = quote.AuthorId, + Txt = quote.Text + }; + + public string Id { get; set; } + public string An { get; set; } + public ulong Aid { get; set; } + public string Txt { get; set; } + } + + private const string _prependExport = + @"# Keys are keywords, Each key has a LIST of quotes in the following format: +# - id: Alphanumeric id used for commands related to the quote. (Note, when using .quotesimport, a new id will be generated.) +# an: Author name +# aid: Author id +# txt: Quote text +"; + private static readonly ISerializer _exportSerializer = new SerializerBuilder() + .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args)) + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) + .WithIndentedSequences() + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) + .DisableAliases() + .Build(); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.Administrator)] + public async Task QuotesExport() + { + IEnumerable quotes; + using (var uow = _db.GetDbContext()) + { + quotes = uow.Quotes + .GetForGuild(ctx.Guild.Id) + .ToList(); + } + + var crsDict = quotes + .GroupBy(x => x.Keyword) + .ToDictionary(x => x.Key, x => x.Select(ExportedQuote.FromModel)); + + var text = _prependExport + _exportSerializer + .Serialize(crsDict) + .UnescapeUnicodeCodePoints(); + + await using var stream = await text.ToStream(); + await ctx.Channel.SendFileAsync(stream, "quote-export.yml", text: null); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [RequireUserPermission(GuildPermission.Administrator)] + [Ratelimit(300)] +#if GLOBAL_NADEKO + [OwnerOnly] +#endif + public async Task QuotesImport([Leftover]string input = null) + { + input = input?.Trim(); + + _ = ctx.Channel.TriggerTypingAsync(); + + if (input is null) + { + var attachment = ctx.Message.Attachments.FirstOrDefault(); + if (attachment is null) + { + await ReplyErrorLocalizedAsync(strs.expr_import_no_input); + return; + } + + using var client = _http.CreateClient(); + input = await client.GetStringAsync(attachment.Url); + + if (string.IsNullOrWhiteSpace(input)) + { + await ReplyErrorLocalizedAsync(strs.expr_import_no_input); + return; + } + } + + var succ = await ImportCrsAsync(ctx.Guild.Id, input); + if (!succ) + { + await ReplyErrorLocalizedAsync(strs.expr_import_invalid_data); + return; + } + + await ctx.OkAsync(); + } + + public async Task ImportCrsAsync(ulong guildId, string input) + { + Dictionary> data; + try + { + data = Yaml.Deserializer.Deserialize>>(input); + if (data.Sum(x => x.Value.Count) == 0) + return false; + } + catch + { + return false; + } + + await using var uow = _db.GetDbContext(); + foreach (var entry in data) + { + var keyword = entry.Key; + await uow.Quotes + .AddRangeAsync(entry.Value + .Where(quote => !string.IsNullOrWhiteSpace(quote.Txt)) + .Select(quote => new Quote() + { + GuildId = guildId, + Keyword = keyword, + Text = quote.Txt, + AuthorId = quote.Aid, + AuthorName = quote.An, + })); + } + + await uow.SaveChangesAsync(); + return true; + } } } } diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 382da20b1..7c14f0edd 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -1264,4 +1264,10 @@ imageonlychannel: - imageonly - imagesonly coordreload: - - coordreload \ No newline at end of file + - coordreload +quotesexport: + - quotesexport + - qexport +quotesimport: + - quotesimport + - qimport \ No newline at end of file diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index 71e4ff39b..ed55d9dd0 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -1578,6 +1578,14 @@ crsexport: desc: "Exports custom reactions from the current server (or global custom reactions in DMs) into a .yml file" args: - "" +quotesimport: + desc: "Upload the file or send the raw .yml data with this command to import all quotes from the specified string or file into the current server." + args: + - "" +quotesexport: + desc: "Exports quotes from the current server into a .yml file" + args: + - "" aliaslist: desc: "Shows the list of currently set aliases. Paginated." args: @@ -2068,7 +2076,7 @@ rollduel: - "50 @Someone" - "@Challenger" reactionroles: - desc: "Specify role names and server emojis with which they're represented, the bot will then add those emojis to the previous message in the channel, and users will be able to get the roles by clicking on the emoji. You can set 'excl' as the parameter before the reactions and roles to make them exclusive. You can have up to 5 of these enabled on one server at a time. Optionally you can specify target message if you don't want it to be the previous one." + desc: "Specify role names and server emojis with which they're represented, the bot will then add those emojis to the previous message in the channel, and users will be able to get the roles by clicking on the emoji. You can set 'excl' as the parameter before the reactions and roles to make them exclusive. You can have up to 10 of these enabled on one server at a time. Optionally you can specify target message if you don't want it to be the previous one." args: - "Gamer :SomeServerEmoji: Streamer :Other: Watcher :Other2:" - "excl Horde :Horde: Alliance :Alliance:" diff --git a/src/NadekoBot/data/strings/responses/responses.en-US.json b/src/NadekoBot/data/strings/responses/responses.en-US.json index 89296c7ae..21e406bce 100644 --- a/src/NadekoBot/data/strings/responses/responses.en-US.json +++ b/src/NadekoBot/data/strings/responses/responses.en-US.json @@ -682,7 +682,7 @@ "clpa_obsolete": ":tada: **Patreon currency rewards are now automatic!** :tada:\nThis command is now obsolete.\nIf you did not receive your reward for this month's pledge, below are some of the reasons as to why that might be.", "clpa_fail_already": "Maybe you've already received your reward for this month. You can receive rewards only once a month unless you increase your pledge.\nYou can check it by using `.curtrs` command.", "clpa_fail_already_title": "Already rewarded", - "clpa_fail_conn": "Your discord account might not be connected to Patreon. If you are unsure what that means, or don't know how to connect it - you have to go to [Patreon account settings page](https://patreon.com/settings/account) and click 'Connect to discord' button.", + "clpa_fail_conn": "Your discord account might not be connected to Patreon. If you are unsure what that means, or don't know how to connect it - you have to go to [Patreon account settings page](https://www.patreon.com/settings/apps) and click 'Connect to discord' button.", "clpa_fail_conn_title": "Discord account not connected", "clpa_fail_sup": "In order to be eligible for the reward, you must support the project on patreon. You can use {0} command to get the link.", "clpa_fail_sup_title": "Not supporting",