WHEW. Added placeholders in embeds and quotes, added docs about it to explained features. Wrote a placeholder system and fixed some bugs

This commit is contained in:
Master Kwoth 2017-06-28 02:44:30 +02:00
parent aa5b7f96c7
commit 942f15cf05
14 changed files with 328 additions and 210 deletions

View File

@ -36,14 +36,4 @@ For example:
Now if you try to trigger `/o/`, it won't print anything. Now if you try to trigger `/o/`, it won't print anything.
###Placeholders! ###Placeholders!
There are currently three different placeholders which we will look at, with more placeholders potentially coming in the future. To learn about placeholders, go [here](Placeholders.md)
| 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

24
docs/Placeholders.md Normal file
View File

@ -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)

View File

@ -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) - [Permissions System](Permissions System.md)
- [JSON Explanations](JSON Explanations.md) - [JSON Explanations](JSON Explanations.md)
- [Custom Reactions](Custom Reactions.md) - [Custom Reactions](Custom Reactions.md)
- [Placeholders](Placeholders.md)
- [Frequently Asked Questions](Frequently Asked Questions.md) - [Frequently Asked Questions](Frequently Asked Questions.md)
- [Contribution Guide](Contribution Guide.md) - [Contribution Guide](Contribution Guide.md)
- [Donate](Donate.md) - [Donate](Donate.md)

View File

@ -14,9 +14,10 @@ pages:
- Readme: Readme.md - Readme: Readme.md
- Commands List: Commands List.md - Commands List: Commands List.md
- Features Explained: - Features Explained:
- Permissions System: Permissions System.md
- JSON Explanations: JSON Explanations.md - JSON Explanations: JSON Explanations.md
- Permissions System: Permissions System.md
- Custom Reactions: Custom Reactions.md - Custom Reactions: Custom Reactions.md
- Placeholders: Placeholders.md
- Frequently Asked Questions: Frequently Asked Questions.md - Frequently Asked Questions: Frequently Asked Questions.md
- Contribution Guide: Contribution Guide.md - Contribution Guide: Contribution Guide.md
- ❤ Donate ❤: Donate.md - ❤ Donate ❤: Donate.md

View File

@ -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(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%", RegexOptions.Compiled);
private ConcurrentDictionary<string, Func<string>> _reps = new ConcurrentDictionary<string, Func<string>>();
private ConcurrentDictionary<Regex, Func<Match, string>> _regex = new ConcurrentDictionary<Regex, Func<Match, string>>();
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<string> 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());
}
}
}

View File

@ -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<string> Text)> _replacements;
private readonly IEnumerable<(Regex Regex, Func<Match, string> Replacement)> _regex;
public Replacer(IEnumerable<(string, Func<string>)> replacements, IEnumerable<(Regex, Func<Match, string>)> 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);
}
}
}

View File

@ -33,11 +33,11 @@ namespace NadekoBot.Modules.Administration
{ {
var config = uow.BotConfig.GetOrCreate(); var config = uow.BotConfig.GetOrCreate();
_service.RotatingStatuses = config.RotatingStatuses = !config.RotatingStatuses; config.RotatingStatuses = !config.RotatingStatuses;
uow.Complete(); uow.Complete();
} }
} }
if (_service.RotatingStatuses) if (_service.BotConfig.RotatingStatuses)
await ReplyConfirmLocalized("ropl_enabled").ConfigureAwait(false); await ReplyConfirmLocalized("ropl_enabled").ConfigureAwait(false);
else else
await ReplyConfirmLocalized("ropl_disabled").ConfigureAwait(false); await ReplyConfirmLocalized("ropl_disabled").ConfigureAwait(false);
@ -52,7 +52,6 @@ namespace NadekoBot.Modules.Administration
var config = uow.BotConfig.GetOrCreate(); var config = uow.BotConfig.GetOrCreate();
var toAdd = new PlayingStatus { Status = status }; var toAdd = new PlayingStatus { Status = status };
config.RotatingStatusMessages.Add(toAdd); config.RotatingStatusMessages.Add(toAdd);
_service.RotatingStatusMessages.Add(toAdd);
await uow.CompleteAsync(); await uow.CompleteAsync();
} }
@ -63,13 +62,13 @@ namespace NadekoBot.Modules.Administration
[OwnerOnly] [OwnerOnly]
public async Task ListPlaying() public async Task ListPlaying()
{ {
if (!_service.RotatingStatusMessages.Any()) if (!_service.BotConfig.RotatingStatusMessages.Any())
await ReplyErrorLocalized("ropl_not_set").ConfigureAwait(false); await ReplyErrorLocalized("ropl_not_set").ConfigureAwait(false);
else else
{ {
var i = 1; var i = 1;
await ReplyConfirmLocalized("ropl_list", 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); .ConfigureAwait(false);
} }
@ -90,7 +89,6 @@ namespace NadekoBot.Modules.Administration
return; return;
msg = config.RotatingStatusMessages[index].Status; msg = config.RotatingStatusMessages[index].Status;
config.RotatingStatusMessages.RemoveAt(index); config.RotatingStatusMessages.RemoveAt(index);
_service.RotatingStatusMessages.RemoveAt(index);
await uow.CompleteAsync(); await uow.CompleteAsync();
} }
await ReplyConfirmLocalized("reprm", msg).ConfigureAwait(false); await ReplyConfirmLocalized("reprm", msg).ConfigureAwait(false);

View File

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NadekoBot.DataStructures; using NadekoBot.DataStructures;
using NadekoBot.DataStructures.Replacements;
namespace NadekoBot.Modules.Utility namespace NadekoBot.Modules.Utility
{ {
@ -66,19 +67,14 @@ namespace NadekoBot.Modules.Utility
if (quote == null) if (quote == null)
return; return;
CREmbed crembed; if (CREmbed.TryParse(quote.Text, out var crembed))
if (CREmbed.TryParse(quote.Text, out crembed))
{ {
try new ReplacementBuilder()
{ .WithDefault(Context)
await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? "") .Build()
.Replace(crembed);
await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? "")
.ConfigureAwait(false); .ConfigureAwait(false);
}
catch (Exception ex)
{
_log.Warn("Sending CREmbed failed");
_log.Warn(ex);
}
return; return;
} }
await Context.Channel.SendMessageAsync($"`#{quote.Id}` 📣 " + quote.Text.SanitizeMentions()); await Context.Channel.SendMessageAsync($"`#{quote.Id}` 📣 " + quote.Text.SanitizeMentions());
@ -118,29 +114,27 @@ namespace NadekoBot.Modules.Utility
using (var uow = _db.UnitOfWork) using (var uow = _db.UnitOfWork)
{ {
var qfromid = uow.Quotes.Get(id); var qfromid = uow.Quotes.Get(id);
CREmbed crembed;
if (qfromid == null) if (qfromid == null)
{ {
await Context.Channel.SendErrorAsync(GetText("quotes_notfound")); 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 new ReplacementBuilder()
{ .WithDefault(Context)
await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText ?? "") .Build()
.Replace(crembed);
await Context.Channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? "")
.ConfigureAwait(false); .ConfigureAwait(false);
} }
catch (Exception ex) else
{ {
_log.Warn("Sending CREmbed failed"); await Context.Channel.SendMessageAsync($"`#{qfromid.Id}` 🗯️ " + qfromid.Keyword.ToLowerInvariant().SanitizeMentions() + ": " +
_log.Warn(ex); qfromid.Text.SanitizeMentions());
}
return;
} }
else { await Context.Channel.SendMessageAsync($"`#{qfromid.Id}` 🗯️ " + qfromid.Keyword.ToLowerInvariant().SanitizeMentions() + ": " +
qfromid.Text.SanitizeMentions()); }
} }
} }

View File

@ -199,7 +199,7 @@ namespace NadekoBot
var muteService = new MuteService(Client, AllGuildConfigs, Db); var muteService = new MuteService(Client, AllGuildConfigs, Db);
var ratelimitService = new SlowmodeService(Client, AllGuildConfigs); var ratelimitService = new SlowmodeService(Client, AllGuildConfigs);
var protectionService = new ProtectionService(Client, AllGuildConfigs, muteService); 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 gameVcService = new GameVoiceChannelService(Client, Db, AllGuildConfigs);
var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs); var autoAssignRoleService = new AutoAssignRoleService(Client, AllGuildConfigs);
var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService); var logCommandService = new LogCommandService(Client, Strings, AllGuildConfigs, Db, muteService, protectionService);

View File

@ -1377,15 +1377,6 @@
<data name="typeadd_usage" xml:space="preserve"> <data name="typeadd_usage" xml:space="preserve">
<value>`{0}typeadd wordswords`</value> <value>`{0}typeadd wordswords`</value>
</data> </data>
<data name="poll_cmd" xml:space="preserve">
<value>poll</value>
</data>
<data name="poll_desc" xml:space="preserve">
<value>Creates a poll which requires users to send the number of the voting option to the bot.</value>
</data>
<data name="poll_usage" xml:space="preserve">
<value>`{0}poll Question?;Answer1;Answ 2;A_3`</value>
</data>
<data name="pollend_cmd" xml:space="preserve"> <data name="pollend_cmd" xml:space="preserve">
<value>pollend</value> <value>pollend</value>
</data> </data>
@ -2583,13 +2574,13 @@
<data name="togethertube_usage" xml:space="preserve"> <data name="togethertube_usage" xml:space="preserve">
<value>`{0}totube`</value> <value>`{0}totube`</value>
</data> </data>
<data name="publicpoll_cmd" xml:space="preserve"> <data name="poll_cmd" xml:space="preserve">
<value>publicpoll ppoll</value> <value>poll ppoll</value>
</data> </data>
<data name="publicpoll_desc" xml:space="preserve"> <data name="poll_desc" xml:space="preserve">
<value>Creates a public poll which requires users to type a number of the voting option in the channel command is ran in.</value> <value>Creates a public poll which requires users to type a number of the voting option in the channel command is ran in.</value>
</data> </data>
<data name="publicpoll_usage" xml:space="preserve"> <data name="poll_usage" xml:space="preserve">
<value>`{0}ppoll Question?;Answer1;Answ 2;A_3`</value> <value>`{0}ppoll Question?;Answer1;Answ 2;A_3`</value>
</data> </data>
<data name="autotranslang_cmd" xml:space="preserve"> <data name="autotranslang_cmd" xml:space="preserve">

View File

@ -1,59 +1,65 @@
using Discord.WebSocket; using Discord.WebSocket;
using NadekoBot.Extensions; using NadekoBot.DataStructures.Replacements;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using NadekoBot.Services.Music; using NadekoBot.Services.Music;
using NLog; using NLog;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
namespace NadekoBot.Services.Administration 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 class PlayingRotateService
{ {
public List<PlayingStatus> RotatingStatusMessages { get; }
public volatile bool RotatingStatuses;
private readonly Timer _t; private readonly Timer _t;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly BotConfig _bc;
private readonly MusicService _music; private readonly MusicService _music;
private readonly Logger _log; 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 private class TimerState
{ {
public int Index { get; set; } 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; _client = client;
_bc = bc; BotConfig = bc;
_music = music; _music = music;
_db = db;
_log = LogManager.GetCurrentClassLogger(); _log = LogManager.GetCurrentClassLogger();
_rep = new ReplacementBuilder()
RotatingStatusMessages = _bc.RotatingStatusMessages; .WithClient(client)
RotatingStatuses = _bc.RotatingStatuses; .WithStats(client)
.WithMusic(music)
.Build();
_t = new Timer(async (objState) => _t = new Timer(async (objState) =>
{ {
try try
{ {
using (var uow = _db.UnitOfWork)
{
BotConfig = uow.BotConfig.GetOrCreate();
}
var state = (TimerState)objState; var state = (TimerState)objState;
if (!RotatingStatuses) if (!BotConfig.RotatingStatuses)
return; return;
if (state.Index >= RotatingStatusMessages.Count) if (state.Index >= BotConfig.RotatingStatusMessages.Count)
state.Index = 0; state.Index = 0;
if (!RotatingStatusMessages.Any()) if (!BotConfig.RotatingStatusMessages.Any())
return; return;
var status = RotatingStatusMessages[state.Index++].Status; var status = BotConfig.RotatingStatusMessages[state.Index++].Status;
if (string.IsNullOrWhiteSpace(status)) if (string.IsNullOrWhiteSpace(status))
return; 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); } try { await client.SetGameAsync(status).ConfigureAwait(false); }
catch (Exception ex) catch (Exception ex)
{ {
@ -66,31 +72,5 @@ namespace NadekoBot.Services.Administration
} }
}, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); }, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
} }
public Dictionary<string, Func<DiscordSocketClient, MusicService, string>> PlayingPlaceholders { get; } =
new Dictionary<string, Func<DiscordSocketClient, MusicService, string>> {
{ "%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<string, Func<DiscordSocketClient, string>> ShardSpecificPlaceholders { get; } =
new Dictionary<string, Func<DiscordSocketClient, string>> {
{ "%shardid%", (client) => client.ShardId.ToString()},
{ "%shardguilds%", (client) => client.Guilds.Count.ToString()},
};
} }
} }

View File

@ -3,6 +3,7 @@ using AngleSharp.Dom.Html;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using NadekoBot.DataStructures; using NadekoBot.DataStructures;
using NadekoBot.DataStructures.Replacements;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using System; using System;
@ -15,62 +16,12 @@ namespace NadekoBot.Services.CustomReactions
{ {
public static class Extensions public static class Extensions
{ {
public static Dictionary<string, Func<IUserMessage, string, string>> responsePlaceholders = new Dictionary<string, Func<IUserMessage, string, string>>()
{
{"%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<string, Func<IUserMessage, DiscordSocketClient, string>> placeholders = new Dictionary<string, Func<IUserMessage, DiscordSocketClient, string>>()
{
{"%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(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%", RegexOptions.Compiled);
private static readonly Regex imgRegex = new Regex("%(img|image):(?<tag>.*?)%", RegexOptions.Compiled); private static readonly Regex imgRegex = new Regex("%(img|image):(?<tag>.*?)%", RegexOptions.Compiled);
private static readonly NadekoRandom rng = new NadekoRandom(); private static readonly NadekoRandom rng = new NadekoRandom();
public static Dictionary<Regex, Func<Match, Task<string>>> regexPlaceholders = new Dictionary<Regex, Func<Match, Task<string>>>() public static Dictionary<Regex, Func<Match, Task<string>>> regexPlaceholders = new Dictionary<Regex, Func<Match, Task<string>>>()
{ {
{ 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) => { { imgRegex, async (match) => {
var tag = match.Groups["tag"].ToString(); var tag = match.Groups["tag"].ToString();
if(string.IsNullOrWhiteSpace(tag)) if(string.IsNullOrWhiteSpace(tag))
@ -96,29 +47,24 @@ namespace NadekoBot.Services.CustomReactions
private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordSocketClient client) private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordSocketClient client)
{ {
foreach (var ph in placeholders) var rep = new ReplacementBuilder()
{ .WithUser(ctx.Author)
if (str.Contains(ph.Key)) .WithClient(client)
str = str.ToLowerInvariant().Replace(ph.Key, ph.Value(ctx, client)); .Build();
}
str = rep.Replace(str.ToLowerInvariant());
return str; return str;
} }
private static async Task<string> ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger) private static async Task<string> ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger)
{ {
foreach (var ph in placeholders) var rep = new ReplacementBuilder()
{ .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client)
var lowerKey = ph.Key.ToLowerInvariant(); .WithOverride("%target%", () => ctx.Content.Substring(resolvedTrigger.Length).Trim())
if (str.Contains(lowerKey)) .Build();
str = str.Replace(lowerKey, ph.Value(ctx, client));
}
foreach (var ph in responsePlaceholders) str = rep.Replace(str.ToLowerInvariant());
{
var lowerKey = ph.Key.ToLowerInvariant();
if (str.Contains(lowerKey))
str = str.Replace(lowerKey, ph.Value(ctx, resolvedTrigger));
}
foreach (var ph in regexPlaceholders) foreach (var ph in regexPlaceholders)
{ {
@ -133,17 +79,24 @@ namespace NadekoBot.Services.CustomReactions
public static Task<string > ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client) public static Task<string > ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client)
=> cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client)); => cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client));
public static async Task<IUserMessage> Send(this CustomReaction cr, IUserMessage context, DiscordSocketClient client, CustomReactionsService crs) public static async Task<IUserMessage> 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); crs.ReactionStats.AddOrUpdate(cr.Trigger, 1, (k, old) => ++old);
if (CREmbed.TryParse(cr.Response, out CREmbed crembed)) 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());
} }
} }
} }

View File

@ -1,6 +1,7 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using NadekoBot.DataStructures; using NadekoBot.DataStructures;
using NadekoBot.DataStructures.Replacements;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using NLog; using NLog;
@ -45,15 +46,17 @@ namespace NadekoBot.Services
if (channel == null) //maybe warn the server owner that the channel is missing if (channel == null) //maybe warn the server owner that the channel is missing
return; 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); rep.Replace(embedData);
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);
try 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) if (conf.AutoDeleteByeMessagesTimer > 0)
{ {
toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer);
@ -63,7 +66,7 @@ namespace NadekoBot.Services
} }
else 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)) if (string.IsNullOrWhiteSpace(msg))
return; return;
try try
@ -98,16 +101,16 @@ namespace NadekoBot.Services
var channel = (await user.Guild.GetTextChannelsAsync()).SingleOrDefault(c => c.Id == conf.GreetMessageChannelId); 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 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 var embedData))
if (CREmbed.TryParse(conf.ChannelGreetMessageText, out embedData))
{ {
embedData.PlainText = embedData.PlainText?.Replace("%user%", user.Mention).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name); rep.Replace(embedData);
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);
try 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) if (conf.AutoDeleteGreetMessagesTimer > 0)
{ {
toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
@ -117,7 +120,7 @@ namespace NadekoBot.Services
} }
else 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)) if (!string.IsNullOrWhiteSpace(msg))
{ {
try try
@ -140,21 +143,22 @@ namespace NadekoBot.Services
if (channel != null) if (channel != null)
{ {
CREmbed embedData; var rep = new ReplacementBuilder()
if (CREmbed.TryParse(conf.DmGreetMessageText, out embedData)) .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); rep.Replace(embedData);
embedData.Title = embedData.Title?.Replace("%user%", user.ToString()).Replace("%id%", user.Id.ToString()).Replace("%server%", user.Guild.Name);
try 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); } catch (Exception ex) { _log.Warn(ex); }
} }
else 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)) if (!string.IsNullOrWhiteSpace(msg))
{ {
await channel.SendConfirmAsync(msg).ConfigureAwait(false); await channel.SendConfirmAsync(msg).ConfigureAwait(false);

View File

@ -1,5 +1,6 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using NadekoBot.DataStructures.Replacements;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using NadekoBot.Services.Database; using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
@ -20,13 +21,6 @@ namespace NadekoBot.Services.Utility
public string RemindMessageFormat { get; } public string RemindMessageFormat { get; }
public readonly IDictionary<string, Func<Reminder, string>> _replacements = new Dictionary<string, Func<Reminder, string>>
{
{ "%message%" , (r) => r.Message },
{ "%user%", (r) => $"<@!{r.UserId}>" },
{ "%target%", (r) => r.IsPrivate ? "Direct Message" : $"<#{r.ChannelId}>"}
};
private readonly Logger _log; private readonly Logger _log;
private readonly CancellationTokenSource cancelSource; private readonly CancellationTokenSource cancelSource;
private readonly CancellationToken cancelAllToken; private readonly CancellationToken cancelAllToken;
@ -82,11 +76,13 @@ namespace NadekoBot.Services.Utility
if (ch == null) if (ch == null)
return; return;
await ch.SendMessageAsync( var rep = new ReplacementBuilder()
_replacements.Aggregate(RemindMessageFormat, .WithOverride("%user%", () => $"<@!{r.UserId}>")
(cur, replace) => cur.Replace(replace.Key, replace.Value(r))) .WithOverride("%message%", () => r.Message)
.SanitizeMentions() .WithOverride("%target%", () => r.IsPrivate ? "Direct Message" : $"<#{r.ChannelId}>")
).ConfigureAwait(false); //it works trust me .Build();
await ch.SendMessageAsync(rep.Replace(RemindMessageFormat).SanitizeMentions()).ConfigureAwait(false); //it works trust me
} }
catch (Exception ex) { _log.Warn(ex); } catch (Exception ex) { _log.Warn(ex); }
finally finally