From 942f15cf0512b8997a24493ae5593ef2e69fd932 Mon Sep 17 00:00:00 2001 From: Master Kwoth Date: Wed, 28 Jun 2017 02:44:30 +0200 Subject: [PATCH] WHEW. Added placeholders in embeds and quotes, added docs about it to explained features. Wrote a placeholder system and fixed some bugs --- docs/Custom Reactions.md | 12 +- docs/Placeholders.md | 24 ++++ docs/index.md | 1 + mkdocs.yml | 3 +- .../Replacements/ReplacementBuilder.cs | 132 ++++++++++++++++++ .../DataStructures/Replacements/Replacer.cs | 54 +++++++ .../Commands/PlayingRotateCommands.cs | 10 +- .../Modules/Utility/Commands/QuoteCommands.cs | 52 +++---- src/NadekoBot/NadekoBot.cs | 2 +- src/NadekoBot/Resources/CommandStrings.resx | 17 +-- .../Administration/PlayingRotateService.cs | 68 ++++----- .../Services/CustomReactions/Extensions.cs | 95 ++++--------- .../Services/GreetSettingsService.cs | 48 ++++--- .../Services/Utility/RemindService.cs | 20 ++- 14 files changed, 328 insertions(+), 210 deletions(-) create mode 100644 docs/Placeholders.md create mode 100644 src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs create mode 100644 src/NadekoBot/DataStructures/Replacements/Replacer.cs diff --git a/docs/Custom Reactions.md b/docs/Custom Reactions.md index 8d41afca..7926028d 100644 --- a/docs/Custom Reactions.md +++ b/docs/Custom Reactions.md @@ -36,14 +36,4 @@ For example: Now if you try to trigger `/o/`, it won't print anything. ###Placeholders! -There are currently three different placeholders which we will look at, with more placeholders potentially coming in the future. - -| Placeholder | Description | Example Usage | Usage | -|:-----------:|-------------|---------------|-------| -|`%mention%`|The `%mention%` placeholder is triggered when you type `@BotName` - It's important to note that if you've given the bot a custom nickname, this trigger won't work!|```.acr "Hello %mention%" I, %mention%, also say hello!```|Input: "Hello @BotName" Output: "I, @BotName, also say hello!"| -|`%user%`|The `%user%` placeholder mentions the person who said the command|`.acr "Who am I?" You are %user%!`|Input: "Who am I?" Output: "You are @Username!"| -|`%rng%`|The `%rng%` placeholder generates a random number between 0 and 10. You can also specify a custom range (%rng1-100%) even with negative numbers: `%rng-9--1%` (from -9 to -1) . |`.acr "Random number" %rng%`|Input: "Random number" Output: "2"| -|`%rnduser%`|The `%rnduser%` placeholder mentions a random user from the server. |`.acr "Random user" %rnduser%`|Input: "Random number" Output: @SomeUser| -|`%target%`|The `%target%` placeholder is used to make Nadeko Mention another person or phrase, it is only supported as part of the response|`.acr "Say this: " %target%`|Input: "Say this: I, @BotName, am a parrot!". Output: "I, @BotName, am a parrot!".| - - Thanks to Nekai for being creative. <3 +To learn about placeholders, go [here](Placeholders.md) diff --git a/docs/Placeholders.md b/docs/Placeholders.md new file mode 100644 index 00000000..0039e0a3 --- /dev/null +++ b/docs/Placeholders.md @@ -0,0 +1,24 @@ +Placeholders are used in Quotes, Custom Reactions, Greet/bye messages, playing statuses, and a few other places. + +They can be used to make the message more user friendly, generate random numbers or pictures, etc... + +Some features have their own specific placeholders which are noted in that feature's command help. Some placeholders are not available in certain features because they don't make sense there. + +### Here is a list of the usual placeholders: +- `%mention%` - Mention the bot +- `%shardid%` - Shard id +- `%server%` - Server name +- `%sid%` - Server Id +- `%channel%` - Channel mention +- `%chname%` - Channel mention +- `%cid%` - Channel Id +- `%user%` - User mention +- `%id%` or `%uid%` - User Id +- `%userfull%` - Username#discriminator +- `%userdiscrim%` - discriminator (for example 1234) +- `%rngX-Y%` - Replace X and Y with the range (for example `%rng5-10%` - random between 5 and 10) +- `%time%` - Bot time + +**If you're using placeholders in embeds, don't use %user% and in titles, footers and field names. They will not show properly.** + +![img](http://i.imgur.com/lNNNfs1.png) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 5e67f8dc..b7bd5ed4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,7 @@ If you want to contribute, be sure to PR on the **[1.4][1.4]** branch. - [Permissions System](Permissions System.md) - [JSON Explanations](JSON Explanations.md) - [Custom Reactions](Custom Reactions.md) + - [Placeholders](Placeholders.md) - [Frequently Asked Questions](Frequently Asked Questions.md) - [Contribution Guide](Contribution Guide.md) - [Donate](Donate.md) diff --git a/mkdocs.yml b/mkdocs.yml index e8ccbbad..0ab5e11b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,9 +14,10 @@ pages: - Readme: Readme.md - Commands List: Commands List.md - Features Explained: - - Permissions System: Permissions System.md - JSON Explanations: JSON Explanations.md + - Permissions System: Permissions System.md - Custom Reactions: Custom Reactions.md + - Placeholders: Placeholders.md - Frequently Asked Questions: Frequently Asked Questions.md - Contribution Guide: Contribution Guide.md - ❤ Donate ❤: Donate.md diff --git a/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs new file mode 100644 index 00000000..b7fabd25 --- /dev/null +++ b/src/NadekoBot/DataStructures/Replacements/ReplacementBuilder.cs @@ -0,0 +1,132 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using NadekoBot.Extensions; +using NadekoBot.Services; +using NadekoBot.Services.Music; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Text.RegularExpressions; + +namespace NadekoBot.DataStructures.Replacements +{ + public class ReplacementBuilder + { + private static readonly Regex rngRegex = new Regex("%rng(?:(?(?:-)?\\d+)-(?(?:-)?\\d+))?%", RegexOptions.Compiled); + private ConcurrentDictionary> _reps = new ConcurrentDictionary>(); + private ConcurrentDictionary> _regex = new ConcurrentDictionary>(); + + public ReplacementBuilder() + { + WithRngRegex(); + } + + public ReplacementBuilder WithDefault(IUser usr, IMessageChannel ch, IGuild g, DiscordSocketClient client) + { + return this.WithUser(usr) + .WithChannel(ch) + .WithServer(g) + .WithClient(client); + } + + public ReplacementBuilder WithDefault(ICommandContext ctx) => + WithDefault(ctx.User, ctx.Channel, ctx.Guild, (DiscordSocketClient)ctx.Client); + + public ReplacementBuilder WithClient(DiscordSocketClient client) + { + _reps.TryAdd("%mention%", () => $"<@{client.CurrentUser.Id}>"); + _reps.TryAdd("%shardid%", () => client.ShardId.ToString()); + _reps.TryAdd("%time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials())); + return this; + } + + public ReplacementBuilder WithServer(IGuild g) + { + _reps.TryAdd("%sid%", () => g == null ? "DM" : g.Id.ToString()); + _reps.TryAdd("%server%", () => g == null ? "DM" : g.Name); + return this; + } + + public ReplacementBuilder WithChannel(IMessageChannel ch) + { + _reps.TryAdd("%channel%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name); + _reps.TryAdd("%chname%", () => ch.Name); + _reps.TryAdd("%cid%", () => ch?.Id.ToString()); + return this; + } + + public ReplacementBuilder WithUser(IUser user) + { + _reps.TryAdd("%user%", () => user.Mention); + _reps.TryAdd("%userfull%", () => user.ToString()); + _reps.TryAdd("%username%", () => user.Username); + _reps.TryAdd("%userdiscrim%", () => user.Discriminator); + _reps.TryAdd("%id%", () => user.Id.ToString()); + _reps.TryAdd("%uid%", () => user.Id.ToString()); + return this; + } + + public ReplacementBuilder WithStats(DiscordSocketClient c) + { + _reps.TryAdd("%servers%", () => c.Guilds.Count.ToString()); + _reps.TryAdd("%users%", () => c.Guilds.Sum(s => s.Users.Count).ToString()); + return this; + } + + public ReplacementBuilder WithMusic(MusicService ms) + { + _reps.TryAdd("%playing%", () => + { + var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); + if (cnt != 1) return cnt.ToString(); + try + { + var mp = ms.MusicPlayers.FirstOrDefault(); + return mp.Value.CurrentSong.SongInfo.Title; + } + catch + { + return "No songs"; + } + }); + _reps.TryAdd("%queued%", () => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()); + return this; + } + + public ReplacementBuilder WithRngRegex() + { + var rng = new NadekoRandom(); + _regex.TryAdd(rngRegex, (match) => + { + int from = 0; + int.TryParse(match.Groups["from"].ToString(), out from); + + int to = 0; + int.TryParse(match.Groups["to"].ToString(), out to); + + if (from == 0 && to == 0) + { + return rng.Next(0, 11).ToString(); + } + + if (from >= to) + return string.Empty; + + return rng.Next(from, to + 1).ToString(); + }); + return this; + } + + public ReplacementBuilder WithOverride(string key, Func output) + { + _reps.AddOrUpdate(key, output, delegate { return output; }); + return this; + } + + public Replacer Build() + { + return new Replacer(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray()); + } + } +} diff --git a/src/NadekoBot/DataStructures/Replacements/Replacer.cs b/src/NadekoBot/DataStructures/Replacements/Replacer.cs new file mode 100644 index 00000000..ec1bbe7e --- /dev/null +++ b/src/NadekoBot/DataStructures/Replacements/Replacer.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace NadekoBot.DataStructures.Replacements +{ + public class Replacer + { + private readonly IEnumerable<(string Key, Func Text)> _replacements; + private readonly IEnumerable<(Regex Regex, Func Replacement)> _regex; + + public Replacer(IEnumerable<(string, Func)> replacements, IEnumerable<(Regex, Func)> regex) + { + _replacements = replacements; + _regex = regex; + } + + public string Replace(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return input; + + foreach (var item in _replacements) + { + if (input.Contains(item.Key)) + input = input.Replace(item.Key, item.Text()); + } + + foreach (var item in _regex) + { + input = item.Regex.Replace(input, (m) => item.Replacement(m)); + } + + return input; + } + + public void Replace(CREmbed embedData) + { + embedData.PlainText = Replace(embedData.PlainText); + embedData.Description = Replace(embedData.Description); + embedData.Title = Replace(embedData.Title); + + if (embedData.Fields != null) + foreach (var f in embedData.Fields) + { + f.Name = Replace(f.Name); + f.Value = Replace(f.Value); + } + + if (embedData.Footer != null) + embedData.Footer.Text = Replace(embedData.Footer.Text); + } + } +} diff --git a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs index 47c39849..867ff91a 100644 --- a/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/Commands/PlayingRotateCommands.cs @@ -33,11 +33,11 @@ namespace NadekoBot.Modules.Administration { var config = uow.BotConfig.GetOrCreate(); - _service.RotatingStatuses = config.RotatingStatuses = !config.RotatingStatuses; + config.RotatingStatuses = !config.RotatingStatuses; uow.Complete(); } } - if (_service.RotatingStatuses) + if (_service.BotConfig.RotatingStatuses) await ReplyConfirmLocalized("ropl_enabled").ConfigureAwait(false); else await ReplyConfirmLocalized("ropl_disabled").ConfigureAwait(false); @@ -52,7 +52,6 @@ namespace NadekoBot.Modules.Administration var config = uow.BotConfig.GetOrCreate(); var toAdd = new PlayingStatus { Status = status }; config.RotatingStatusMessages.Add(toAdd); - _service.RotatingStatusMessages.Add(toAdd); await uow.CompleteAsync(); } @@ -63,13 +62,13 @@ namespace NadekoBot.Modules.Administration [OwnerOnly] public async Task ListPlaying() { - if (!_service.RotatingStatusMessages.Any()) + if (!_service.BotConfig.RotatingStatusMessages.Any()) await ReplyErrorLocalized("ropl_not_set").ConfigureAwait(false); else { var i = 1; await ReplyConfirmLocalized("ropl_list", - string.Join("\n\t", _service.RotatingStatusMessages.Select(rs => $"`{i++}.` {rs.Status}"))) + string.Join("\n\t", _service.BotConfig.RotatingStatusMessages.Select(rs => $"`{i++}.` {rs.Status}"))) .ConfigureAwait(false); } @@ -90,7 +89,6 @@ namespace NadekoBot.Modules.Administration return; msg = config.RotatingStatusMessages[index].Status; config.RotatingStatusMessages.RemoveAt(index); - _service.RotatingStatusMessages.RemoveAt(index); await uow.CompleteAsync(); } await ReplyConfirmLocalized("reprm", msg).ConfigureAwait(false); diff --git a/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs b/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs index 5ac2e69a..dcdc035a 100644 --- a/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs +++ b/src/NadekoBot/Modules/Utility/Commands/QuoteCommands.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NadekoBot.DataStructures; +using NadekoBot.DataStructures.Replacements; namespace NadekoBot.Modules.Utility { @@ -66,19 +67,14 @@ namespace NadekoBot.Modules.Utility if (quote == null) return; - CREmbed crembed; - if (CREmbed.TryParse(quote.Text, out crembed)) + if (CREmbed.TryParse(quote.Text, out var crembed)) { - try - { - await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? "") - .ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn("Sending CREmbed failed"); - _log.Warn(ex); - } + new ReplacementBuilder() + .WithDefault(Context) + .Build() + .Replace(crembed); + await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? "") + .ConfigureAwait(false); return; } await Context.Channel.SendMessageAsync($"`#{quote.Id}` 📣 " + quote.Text.SanitizeMentions()); @@ -118,29 +114,27 @@ namespace NadekoBot.Modules.Utility using (var uow = _db.UnitOfWork) { var qfromid = uow.Quotes.Get(id); - CREmbed crembed; - + if (qfromid == null) { await Context.Channel.SendErrorAsync(GetText("quotes_notfound")); } - else if (CREmbed.TryParse(qfromid.Text, out crembed)) + else if (CREmbed.TryParse(qfromid.Text, out var crembed)) { - try - { - await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? "") - .ConfigureAwait(false); - } - catch (Exception ex) - { - _log.Warn("Sending CREmbed failed"); - _log.Warn(ex); - } - return; + new ReplacementBuilder() + .WithDefault(Context) + .Build() + .Replace(crembed); + + await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? "") + .ConfigureAwait(false); } - - else { await Context.Channel.SendMessageAsync($"`#{qfromid.Id}` 🗯️ " + qfromid.Keyword.ToLowerInvariant().SanitizeMentions() + ": " + - qfromid.Text.SanitizeMentions()); } + else + { + await Context.Channel.SendMessageAsync($"`#{qfromid.Id}` 🗯️ " + qfromid.Keyword.ToLowerInvariant().SanitizeMentions() + ": " + + qfromid.Text.SanitizeMentions()); + } + } } diff --git a/src/NadekoBot/NadekoBot.cs b/src/NadekoBot/NadekoBot.cs index ac63f42a..c75878b3 100644 --- a/src/NadekoBot/NadekoBot.cs +++ b/src/NadekoBot/NadekoBot.cs @@ -199,7 +199,7 @@ namespace NadekoBot var muteService = new MuteService(Client, AllGuildConfigs, Db); var ratelimitService = new SlowmodeService(Client, AllGuildConfigs); var protectionService = new ProtectionService(Client, AllGuildConfigs, muteService); - var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService); + var playingRotateService = new PlayingRotateService(Client, BotConfig, musicService, Db); var gameVcService = new GameVoiceChannelService(Client, Db, AllGuildConfigs); var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService); diff --git a/src/NadekoBot/Resources/CommandStrings.resx b/src/NadekoBot/Resources/CommandStrings.resx index e896535d..27d37eb7 100644 --- a/src/NadekoBot/Resources/CommandStrings.resx +++ b/src/NadekoBot/Resources/CommandStrings.resx @@ -1377,15 +1377,6 @@ `{0}typeadd wordswords` - - poll - - - Creates a poll which requires users to send the number of the voting option to the bot. - - - `{0}poll Question?;Answer1;Answ 2;A_3` - pollend @@ -2583,13 +2574,13 @@ `{0}totube` - - publicpoll ppoll + + poll ppoll - + Creates a public poll which requires users to type a number of the voting option in the channel command is ran in. - + `{0}ppoll Question?;Answer1;Answ 2;A_3` diff --git a/src/NadekoBot/Services/Administration/PlayingRotateService.cs b/src/NadekoBot/Services/Administration/PlayingRotateService.cs index c7a1bf41..9e33aef7 100644 --- a/src/NadekoBot/Services/Administration/PlayingRotateService.cs +++ b/src/NadekoBot/Services/Administration/PlayingRotateService.cs @@ -1,59 +1,65 @@ using Discord.WebSocket; -using NadekoBot.Extensions; +using NadekoBot.DataStructures.Replacements; +using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Services.Music; using NLog; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; namespace NadekoBot.Services.Administration { - //todo 99 - Could make a placeholder service, which can work for any module - //and have replacements which are dependent on the types provided in the constructor public class PlayingRotateService { - public List RotatingStatusMessages { get; } - public volatile bool RotatingStatuses; private readonly Timer _t; private readonly DiscordSocketClient _client; - private readonly BotConfig _bc; private readonly MusicService _music; private readonly Logger _log; + private readonly Replacer _rep; + private readonly DbService _db; + public BotConfig BotConfig { get; private set; } //todo load whole botconifg, not just for this service when you have the time private class TimerState { public int Index { get; set; } } - public PlayingRotateService(DiscordSocketClient client, BotConfig bc, MusicService music) + public PlayingRotateService(DiscordSocketClient client, BotConfig bc, MusicService music, DbService db) { _client = client; - _bc = bc; + BotConfig = bc; _music = music; + _db = db; _log = LogManager.GetCurrentClassLogger(); + _rep = new ReplacementBuilder() + .WithClient(client) + .WithStats(client) + .WithMusic(music) + .Build(); - RotatingStatusMessages = _bc.RotatingStatusMessages; - RotatingStatuses = _bc.RotatingStatuses; - _t = new Timer(async (objState) => { try { + using (var uow = _db.UnitOfWork) + { + BotConfig = uow.BotConfig.GetOrCreate(); + } var state = (TimerState)objState; - if (!RotatingStatuses) + if (!BotConfig.RotatingStatuses) return; - if (state.Index >= RotatingStatusMessages.Count) + if (state.Index >= BotConfig.RotatingStatusMessages.Count) state.Index = 0; - if (!RotatingStatusMessages.Any()) + if (!BotConfig.RotatingStatusMessages.Any()) return; - var status = RotatingStatusMessages[state.Index++].Status; + var status = BotConfig.RotatingStatusMessages[state.Index++].Status; if (string.IsNullOrWhiteSpace(status)) return; - PlayingPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(_client, _music))); - ShardSpecificPlaceholders.ForEach(e => status = status.Replace(e.Key, e.Value(client))); + + status = _rep.Replace(status); + try { await client.SetGameAsync(status).ConfigureAwait(false); } catch (Exception ex) { @@ -66,31 +72,5 @@ namespace NadekoBot.Services.Administration } }, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); } - - public Dictionary> PlayingPlaceholders { get; } = - new Dictionary> { - { "%servers%", (c, ms) => c.Guilds.Count.ToString()}, - { "%users%", (c, ms) => c.Guilds.Sum(s => s.Users.Count).ToString()}, - { "%playing%", (c, ms) => { - var cnt = ms.MusicPlayers.Count(kvp => kvp.Value.CurrentSong != null); - if (cnt != 1) return cnt.ToString(); - try { - var mp = ms.MusicPlayers.FirstOrDefault(); - return mp.Value.CurrentSong.SongInfo.Title; - } - catch { - return "No songs"; - } - } - }, - { "%queued%", (c, ms) => ms.MusicPlayers.Sum(kvp => kvp.Value.Playlist.Count).ToString()}, - { "%time%", (c, ms) => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()) }, - }; - - public Dictionary> ShardSpecificPlaceholders { get; } = - new Dictionary> { - { "%shardid%", (client) => client.ShardId.ToString()}, - { "%shardguilds%", (client) => client.Guilds.Count.ToString()}, - }; } } diff --git a/src/NadekoBot/Services/CustomReactions/Extensions.cs b/src/NadekoBot/Services/CustomReactions/Extensions.cs index 4d6d4d06..6a70241d 100644 --- a/src/NadekoBot/Services/CustomReactions/Extensions.cs +++ b/src/NadekoBot/Services/CustomReactions/Extensions.cs @@ -3,6 +3,7 @@ using AngleSharp.Dom.Html; using Discord; using Discord.WebSocket; using NadekoBot.DataStructures; +using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using System; @@ -15,62 +16,12 @@ namespace NadekoBot.Services.CustomReactions { public static class Extensions { - public static Dictionary> responsePlaceholders = new Dictionary>() - { - {"%target%", (ctx, trigger) => { return ctx.Content.Substring(trigger.Length).Trim().SanitizeMentions(); } }, - {"%rnduser%", (ctx, client) => { - //var ch = ctx.Channel as ITextChannel; - //if(ch == null) - // return ""; - - //var g = ch.Guild as SocketGuild; - //if(g == null) - // return ""; - //try { - // var usr = g.Users.Skip(new NadekoRandom().Next(0, g.Users.Count)).FirstOrDefault(); - // return usr.Mention; - //} - //catch { - return "[%rnduser% is temp. disabled]"; - //} - - //var users = g.Users.ToArray(); - - //return users[new NadekoRandom().Next(0, users.Length-1)].Mention; - } }, - }; - - public static Dictionary> placeholders = new Dictionary>() - { - {"%mention%", (ctx, client) => { return $"<@{client.CurrentUser.Id}>"; } }, - {"%user%", (ctx, client) => { return ctx.Author.Mention; } }, - //{"%rng%", (ctx) => { return new NadekoRandom().Next(0,10).ToString(); } } - }; - - private static readonly Regex rngRegex = new Regex("%rng(?:(?(?:-)?\\d+)-(?(?:-)?\\d+))?%", RegexOptions.Compiled); private static readonly Regex imgRegex = new Regex("%(img|image):(?.*?)%", RegexOptions.Compiled); private static readonly NadekoRandom rng = new NadekoRandom(); public static Dictionary>> regexPlaceholders = new Dictionary>>() { - { rngRegex, (match) => { - int from = 0; - int.TryParse(match.Groups["from"].ToString(), out from); - - int to = 0; - int.TryParse(match.Groups["to"].ToString(), out to); - - if(from == 0 && to == 0) - { - return Task.FromResult(rng.Next(0, 11).ToString()); - } - - if(from >= to) - return Task.FromResult(string.Empty); - - return Task.FromResult(rng.Next(from,to+1).ToString()); - } }, { imgRegex, async (match) => { var tag = match.Groups["tag"].ToString(); if(string.IsNullOrWhiteSpace(tag)) @@ -96,29 +47,24 @@ namespace NadekoBot.Services.CustomReactions private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordSocketClient client) { - foreach (var ph in placeholders) - { - if (str.Contains(ph.Key)) - str = str.ToLowerInvariant().Replace(ph.Key, ph.Value(ctx, client)); - } + var rep = new ReplacementBuilder() + .WithUser(ctx.Author) + .WithClient(client) + .Build(); + + str = rep.Replace(str.ToLowerInvariant()); + return str; } private static async Task ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger) { - foreach (var ph in placeholders) - { - var lowerKey = ph.Key.ToLowerInvariant(); - if (str.Contains(lowerKey)) - str = str.Replace(lowerKey, ph.Value(ctx, client)); - } + var rep = new ReplacementBuilder() + .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) + .WithOverride("%target%", () => ctx.Content.Substring(resolvedTrigger.Length).Trim()) + .Build(); - foreach (var ph in responsePlaceholders) - { - var lowerKey = ph.Key.ToLowerInvariant(); - if (str.Contains(lowerKey)) - str = str.Replace(lowerKey, ph.Value(ctx, resolvedTrigger)); - } + str = rep.Replace(str.ToLowerInvariant()); foreach (var ph in regexPlaceholders) { @@ -133,17 +79,24 @@ namespace NadekoBot.Services.CustomReactions public static Task ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client) => cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client)); - public static async Task Send(this CustomReaction cr, IUserMessage context, DiscordSocketClient client, CustomReactionsService crs) + public static async Task Send(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, CustomReactionsService crs) { - var channel = cr.DmResponse ? await context.Author.CreateDMChannelAsync() : context.Channel; + var channel = cr.DmResponse ? await ctx.Author.CreateDMChannelAsync() : ctx.Channel; crs.ReactionStats.AddOrUpdate(cr.Trigger, 1, (k, old) => ++old); if (CREmbed.TryParse(cr.Response, out CREmbed crembed)) { - return await channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? ""); + var rep = new ReplacementBuilder() + .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) + .WithOverride("%target%", () => ctx.Content.Substring(cr.Trigger.ResolveTriggerString(ctx, client).Length).Trim()) + .Build(); + + rep.Replace(crembed); + + return await channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? ""); } - return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(context, client)).SanitizeMentions()); + return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(ctx, client)).SanitizeMentions()); } } } diff --git a/src/NadekoBot/Services/GreetSettingsService.cs b/src/NadekoBot/Services/GreetSettingsService.cs index 3296c602..b38ac5bd 100644 --- a/src/NadekoBot/Services/GreetSettingsService.cs +++ b/src/NadekoBot/Services/GreetSettingsService.cs @@ -1,6 +1,7 @@ using Discord; using Discord.WebSocket; using NadekoBot.DataStructures; +using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Services.Database.Models; using NLog; @@ -45,15 +46,17 @@ namespace NadekoBot.Services if (channel == null) //maybe warn the server owner that the channel is missing return; - CREmbed embedData; - if (CREmbed.TryParse(conf.ChannelByeMessageText, out embedData)) + + var rep = new ReplacementBuilder() + .WithDefault(user, channel, user.Guild, _client) + .Build(); + + if (CREmbed.TryParse(conf.ChannelByeMessageText, out var embedData)) { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + rep.Replace(embedData); try { - var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText?.SanitizeMentions() ?? "").ConfigureAwait(false); if (conf.AutoDeleteByeMessagesTimer > 0) { toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); @@ -63,7 +66,7 @@ namespace NadekoBot.Services } else { - var msg = conf.ChannelByeMessageText.Replace("%user%", user.Username).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + var msg = rep.Replace(conf.ChannelByeMessageText); if (string.IsNullOrWhiteSpace(msg)) return; try @@ -98,16 +101,16 @@ namespace NadekoBot.Services var channel = (await user.Guild.GetTextChannelsAsync()).SingleOrDefault(c => c.Id == conf.GreetMessageChannelId); if (channel != null) //maybe warn the server owner that the channel is missing { + var rep = new ReplacementBuilder() + .WithDefault(user, channel, user.Guild, _client) + .Build(); - CREmbed embedData; - if (CREmbed.TryParse(conf.ChannelGreetMessageText, out embedData)) + if (CREmbed.TryParse(conf.ChannelGreetMessageText, out var embedData)) { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + rep.Replace(embedData); try { - var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + var toDelete = await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText?.SanitizeMentions() ?? "").ConfigureAwait(false); if (conf.AutoDeleteGreetMessagesTimer > 0) { toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); @@ -117,7 +120,7 @@ namespace NadekoBot.Services } else { - var msg = conf.ChannelGreetMessageText.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + var msg = rep.Replace(conf.ChannelGreetMessageText); if (!string.IsNullOrWhiteSpace(msg)) { try @@ -140,21 +143,22 @@ namespace NadekoBot.Services if (channel != null) { - CREmbed embedData; - if (CREmbed.TryParse(conf.DmGreetMessageText, out embedData)) + var rep = new ReplacementBuilder() + .WithDefault(user, channel, user.Guild, _client) + .Build(); + if (CREmbed.TryParse(conf.DmGreetMessageText, out var embedData)) { - embedData.PlainText = embedData.PlainText?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Description = embedData.Description?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); - embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + + rep.Replace(embedData); try { - await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText ?? "").ConfigureAwait(false); + await channel.EmbedAsync(embedData.ToEmbed(), embedData.PlainText?.SanitizeMentions() ?? "").ConfigureAwait(false); } catch (Exception ex) { _log.Warn(ex); } } else { - var msg = conf.DmGreetMessageText.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); + var msg = rep.Replace(conf.DmGreetMessageText); if (!string.IsNullOrWhiteSpace(msg)) { await channel.SendConfirmAsync(msg).ConfigureAwait(false); @@ -409,4 +413,4 @@ namespace NadekoBot.Services ChannelByeMessageText = g.ChannelByeMessageText, }; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Utility/RemindService.cs b/src/NadekoBot/Services/Utility/RemindService.cs index 8629712d..592564c3 100644 --- a/src/NadekoBot/Services/Utility/RemindService.cs +++ b/src/NadekoBot/Services/Utility/RemindService.cs @@ -1,5 +1,6 @@ using Discord; using Discord.WebSocket; +using NadekoBot.DataStructures.Replacements; using NadekoBot.Extensions; using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; @@ -20,13 +21,6 @@ namespace NadekoBot.Services.Utility public string RemindMessageFormat { get; } - public readonly IDictionary> _replacements = new Dictionary> - { - { "%message%" , (r) => r.Message }, - { "%user%", (r) => $"<@!{r.UserId}>" }, - { "%target%", (r) => r.IsPrivate ? "Direct Message" : $"<#{r.ChannelId}>"} - }; - private readonly Logger _log; private readonly CancellationTokenSource cancelSource; private readonly CancellationToken cancelAllToken; @@ -82,11 +76,13 @@ namespace NadekoBot.Services.Utility if (ch == null) return; - await ch.SendMessageAsync( - _replacements.Aggregate(RemindMessageFormat, - (cur, replace) => cur.Replace(replace.Key, replace.Value(r))) - .SanitizeMentions() - ).ConfigureAwait(false); //it works trust me + var rep = new ReplacementBuilder() + .WithOverride("%user%", () => $"<@!{r.UserId}>") + .WithOverride("%message%", () => r.Message) + .WithOverride("%target%", () => r.IsPrivate ? "Direct Message" : $"<#{r.ChannelId}>") + .Build(); + + await ch.SendMessageAsync(rep.Replace(RemindMessageFormat).SanitizeMentions()).ConfigureAwait(false); //it works trust me } catch (Exception ex) { _log.Warn(ex); } finally