Packages can be loaded/unloaded. IUnloadableService interface added whose method Unload, if service implements it, will be called when the module is unloaded.
This commit is contained in:
		
							
								
								
									
										504
									
								
								NadekoBot.Core/Modules/CustomReactions/CustomReactions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										504
									
								
								NadekoBot.Core/Modules/CustomReactions/CustomReactions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,504 @@ | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Discord.Commands; | ||||
| using NadekoBot.Services; | ||||
| using NadekoBot.Services.Database.Models; | ||||
| using Discord; | ||||
| using NadekoBot.Extensions; | ||||
| using Discord.WebSocket; | ||||
| using System; | ||||
| using NadekoBot.Common.Attributes; | ||||
| using NadekoBot.Modules.CustomReactions.Services; | ||||
|  | ||||
| namespace NadekoBot.Modules.CustomReactions | ||||
| { | ||||
|     public class CustomReactions : NadekoTopLevelModule<CustomReactionsService> | ||||
|     { | ||||
|         private readonly IBotCredentials _creds; | ||||
|         private readonly DbService _db; | ||||
|         private readonly DiscordSocketClient _client; | ||||
|  | ||||
|         public CustomReactions(IBotCredentials creds, DbService db, | ||||
|             DiscordSocketClient client) | ||||
|         { | ||||
|             _creds = creds; | ||||
|             _db = db; | ||||
|             _client = client; | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task AddCustReact(string key, [Remainder] string message) | ||||
|         { | ||||
|             var channel = Context.Channel as ITextChannel; | ||||
|             if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key)) | ||||
|                 return; | ||||
|  | ||||
|             key = key.ToLowerInvariant(); | ||||
|  | ||||
|             if ((channel == null && !_creds.IsOwner(Context.User)) || (channel != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("insuff_perms").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var cr = new CustomReaction() | ||||
|             { | ||||
|                 GuildId = channel?.Guild.Id, | ||||
|                 IsRegex = false, | ||||
|                 Trigger = key, | ||||
|                 Response = message, | ||||
|             }; | ||||
|  | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 uow.CustomReactions.Add(cr); | ||||
|  | ||||
|                 await uow.CompleteAsync().ConfigureAwait(false); | ||||
|             } | ||||
|  | ||||
|             if (channel == null) | ||||
|             { | ||||
|                 await _service.AddGcr(cr).ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _service.GuildReactions.AddOrUpdate(Context.Guild.Id, | ||||
|                     new CustomReaction[] { cr }, | ||||
|                     (k, old) => | ||||
|                     { | ||||
|                         Array.Resize(ref old, old.Length + 1); | ||||
|                         old[old.Length - 1] = cr; | ||||
|                         return old; | ||||
|                     }); | ||||
|             } | ||||
|  | ||||
|             await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() | ||||
|                 .WithTitle(GetText("new_cust_react")) | ||||
|                 .WithDescription($"#{cr.Id}") | ||||
|                 .AddField(efb => efb.WithName(GetText("trigger")).WithValue(key)) | ||||
|                 .AddField(efb => efb.WithName(GetText("response")).WithValue(message.Length > 1024 ? GetText("redacted_too_long") : message)) | ||||
|                 ).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task EditCustReact(int id, [Remainder] string message) | ||||
|         { | ||||
|             var channel = Context.Channel as ITextChannel; | ||||
|             if (string.IsNullOrWhiteSpace(message) || id < 0) | ||||
|                 return; | ||||
|  | ||||
|             if ((channel == null && !_creds.IsOwner(Context.User)) || (channel != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("insuff_perms").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             CustomReaction cr; | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 cr = uow.CustomReactions.Get(id); | ||||
|  | ||||
|                 if (cr != null) | ||||
|                 { | ||||
|                     cr.Response = message; | ||||
|                     await uow.CompleteAsync().ConfigureAwait(false); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (cr != null) | ||||
|             { | ||||
|                 if (channel == null) | ||||
|                 { | ||||
|                     await _service.EditGcr(id, message).ConfigureAwait(false); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     if (_service.GuildReactions.TryGetValue(Context.Guild.Id, out var crs)) | ||||
|                     { | ||||
|                         var oldCr = crs.FirstOrDefault(x => x.Id == id); | ||||
|                         if (oldCr != null) | ||||
|                             oldCr.Response = message; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() | ||||
|                     .WithTitle(GetText("edited_cust_react")) | ||||
|                     .WithDescription($"#{cr.Id}") | ||||
|                     .AddField(efb => efb.WithName(GetText("trigger")).WithValue(cr.Trigger)) | ||||
|                     .AddField(efb => efb.WithName(GetText("response")).WithValue(message.Length > 1024 ? GetText("redacted_too_long") : message)) | ||||
|                     ).ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await ReplyErrorLocalized("edit_fail").ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [Priority(1)] | ||||
|         public async Task ListCustReact(int page = 1) | ||||
|         { | ||||
|             if (--page < 0 || page > 999) | ||||
|                 return; | ||||
|             CustomReaction[] customReactions; | ||||
|             if (Context.Guild == null) | ||||
|                 customReactions = _service.GlobalReactions.Where(cr => cr != null).ToArray(); | ||||
|             else | ||||
|                 customReactions = _service.GuildReactions.GetOrAdd(Context.Guild.Id, Array.Empty<CustomReaction>()).Where(cr => cr != null).ToArray(); | ||||
|  | ||||
|             if (customReactions == null || !customReactions.Any()) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_found").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var lastPage = customReactions.Length / 20; | ||||
|             await Context.Channel.SendPaginatedConfirmAsync(_client, page, curPage => | ||||
|                 new EmbedBuilder().WithOkColor() | ||||
|                     .WithTitle(GetText("name")) | ||||
|                     .WithDescription(string.Join("\n", customReactions.OrderBy(cr => cr.Trigger) | ||||
|                                                     .Skip(curPage * 20) | ||||
|                                                     .Take(20) | ||||
|                                                     .Select(cr => | ||||
|                                                     { | ||||
|                                                         var str = $"`#{cr.Id}` {cr.Trigger}"; | ||||
|                                                         if (cr.AutoDeleteTrigger) | ||||
|                                                         { | ||||
|                                                             str = "🗑" + str; | ||||
|                                                         } | ||||
|                                                         if (cr.DmResponse) | ||||
|                                                         { | ||||
|                                                             str = "📪" + str; | ||||
|                                                         } | ||||
|                                                         return str; | ||||
|                                                     }))), lastPage) | ||||
|                                 .ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         public enum All | ||||
|         { | ||||
|             All | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [Priority(0)] | ||||
|         public async Task ListCustReact(All x) | ||||
|         { | ||||
|             CustomReaction[] customReactions; | ||||
|             if (Context.Guild == null) | ||||
|                 customReactions = _service.GlobalReactions.Where(cr => cr != null).ToArray(); | ||||
|             else | ||||
|                 customReactions = _service.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }).Where(cr => cr != null).ToArray(); | ||||
|  | ||||
|             if (customReactions == null || !customReactions.Any()) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_found").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var txtStream = await customReactions.GroupBy(cr => cr.Trigger) | ||||
|                                                         .OrderBy(cr => cr.Key) | ||||
|                                                         .Select(cr => new { Trigger = cr.Key, Responses = cr.Select(y => new { id = y.Id, text = y.Response }).ToList() }) | ||||
|                                                         .ToJson() | ||||
|                                                         .ToStream() | ||||
|                                                         .ConfigureAwait(false); | ||||
|  | ||||
|             if (Context.Guild == null) // its a private one, just send back | ||||
|                 await Context.Channel.SendFileAsync(txtStream, "customreactions.txt", GetText("list_all")).ConfigureAwait(false); | ||||
|             else | ||||
|                 await ((IGuildUser)Context.User).SendFileAsync(txtStream, "customreactions.txt", GetText("list_all"), false).ConfigureAwait(false); | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task ListCustReactG(int page = 1) | ||||
|         { | ||||
|             if (--page < 0 || page > 9999) | ||||
|                 return; | ||||
|             CustomReaction[] customReactions; | ||||
|             if (Context.Guild == null) | ||||
|                 customReactions = _service.GlobalReactions.Where(cr => cr != null).ToArray(); | ||||
|             else | ||||
|                 customReactions = _service.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }).Where(cr => cr != null).ToArray(); | ||||
|  | ||||
|             if (customReactions == null || !customReactions.Any()) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_found").ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var ordered = customReactions | ||||
|                     .GroupBy(cr => cr.Trigger) | ||||
|                     .OrderBy(cr => cr.Key) | ||||
|                     .ToList(); | ||||
|  | ||||
|                 var lastPage = ordered.Count / 20; | ||||
|                 await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) => | ||||
|                     new EmbedBuilder().WithOkColor() | ||||
|                         .WithTitle(GetText("name")) | ||||
|                         .WithDescription(string.Join("\r\n", ordered | ||||
|                                                          .Skip(curPage * 20) | ||||
|                                                          .Take(20) | ||||
|                                                          .Select(cr => $"**{cr.Key.Trim().ToLowerInvariant()}** `x{cr.Count()}`"))), lastPage) | ||||
|                              .ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task ShowCustReact(int id) | ||||
|         { | ||||
|             CustomReaction[] customReactions; | ||||
|             if (Context.Guild == null) | ||||
|                 customReactions = _service.GlobalReactions; | ||||
|             else | ||||
|                 customReactions = _service.GuildReactions.GetOrAdd(Context.Guild.Id, new CustomReaction[]{ }); | ||||
|  | ||||
|             var found = customReactions.FirstOrDefault(cr => cr?.Id == id); | ||||
|  | ||||
|             if (found == null) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_found_id").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() | ||||
|                     .WithDescription($"#{id}") | ||||
|                     .AddField(efb => efb.WithName(GetText("trigger")).WithValue(found.Trigger)) | ||||
|                     .AddField(efb => efb.WithName(GetText("response")).WithValue(found.Response + "\n```css\n" + found.Response + "```")) | ||||
|                     ).ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task DelCustReact(int id) | ||||
|         { | ||||
|             if ((Context.Guild == null && !_creds.IsOwner(Context.User)) || (Context.Guild != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("insuff_perms").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var success = false; | ||||
|             CustomReaction toDelete; | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 toDelete = uow.CustomReactions.Get(id); | ||||
|                 if (toDelete == null) //not found | ||||
|                     success = false; | ||||
|                 else | ||||
|                 { | ||||
|                     if ((toDelete.GuildId == null || toDelete.GuildId == 0) && Context.Guild == null) | ||||
|                     { | ||||
|                         uow.CustomReactions.Remove(toDelete); | ||||
|                         await _service.DelGcr(toDelete.Id); | ||||
|                         success = true; | ||||
|                     } | ||||
|                     else if ((toDelete.GuildId != null && toDelete.GuildId != 0) && Context.Guild.Id == toDelete.GuildId) | ||||
|                     { | ||||
|                         uow.CustomReactions.Remove(toDelete); | ||||
|                         _service.GuildReactions.AddOrUpdate(Context.Guild.Id, new CustomReaction[] { }, (key, old) => | ||||
|                         { | ||||
|                             return old.Where(cr => cr?.Id != toDelete.Id).ToArray(); | ||||
|                         }); | ||||
|                         success = true; | ||||
|                     } | ||||
|                     if (success) | ||||
|                         await uow.CompleteAsync().ConfigureAwait(false); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (success) | ||||
|             { | ||||
|                 await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor() | ||||
|                     .WithTitle(GetText("deleted")) | ||||
|                     .WithDescription("#" + toDelete.Id) | ||||
|                     .AddField(efb => efb.WithName(GetText("trigger")).WithValue(toDelete.Trigger)) | ||||
|                     .AddField(efb => efb.WithName(GetText("response")).WithValue(toDelete.Response))); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_found_id").ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task CrCa(int id) | ||||
|         { | ||||
|             if ((Context.Guild == null && !_creds.IsOwner(Context.User)) || | ||||
|                 (Context.Guild != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("insuff_perms").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             CustomReaction[] reactions = new CustomReaction[0]; | ||||
|  | ||||
|             if (Context.Guild == null) | ||||
|                 reactions = _service.GlobalReactions; | ||||
|             else | ||||
|             { | ||||
|                 _service.GuildReactions.TryGetValue(Context.Guild.Id, out reactions); | ||||
|             } | ||||
|             if (reactions.Any()) | ||||
|             { | ||||
|                 var reaction = reactions.FirstOrDefault(x => x.Id == id); | ||||
|  | ||||
|                 if (reaction == null) | ||||
|                 { | ||||
|                     await ReplyErrorLocalized("no_found_id").ConfigureAwait(false); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 var setValue = reaction.ContainsAnywhere = !reaction.ContainsAnywhere; | ||||
|                  | ||||
|                 await _service.SetCrCaAsync(reaction.Id, setValue).ConfigureAwait(false); | ||||
|  | ||||
|                 if (setValue) | ||||
|                 { | ||||
|                     await ReplyConfirmLocalized("crca_enabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     await ReplyConfirmLocalized("crca_disabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_found").ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task CrDm(int id) | ||||
|         { | ||||
|             if ((Context.Guild == null && !_creds.IsOwner(Context.User)) ||  | ||||
|                 (Context.Guild != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("insuff_perms").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             CustomReaction[] reactions = new CustomReaction[0]; | ||||
|  | ||||
|             if (Context.Guild == null) | ||||
|                 reactions = _service.GlobalReactions; | ||||
|             else | ||||
|             { | ||||
|                 _service.GuildReactions.TryGetValue(Context.Guild.Id, out reactions); | ||||
|             } | ||||
|             if (reactions.Any()) | ||||
|             { | ||||
|                 var reaction = reactions.FirstOrDefault(x => x.Id == id); | ||||
|  | ||||
|                 if (reaction == null) | ||||
|                 { | ||||
|                     await ReplyErrorLocalized("no_found_id").ConfigureAwait(false); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 var setValue = reaction.DmResponse = !reaction.DmResponse; | ||||
|  | ||||
|                 await _service.SetCrDmAsync(reaction.Id, setValue).ConfigureAwait(false); | ||||
|  | ||||
|                 if (setValue) | ||||
|                 { | ||||
|                     await ReplyConfirmLocalized("crdm_enabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     await ReplyConfirmLocalized("crdm_disabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_found").ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task CrAd(int id) | ||||
|         { | ||||
|             if ((Context.Guild == null && !_creds.IsOwner(Context.User)) || | ||||
|                 (Context.Guild != null && !((IGuildUser)Context.User).GuildPermissions.Administrator)) | ||||
|             { | ||||
|                 await ReplyErrorLocalized("insuff_perms").ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             CustomReaction[] reactions = new CustomReaction[0]; | ||||
|  | ||||
|             if (Context.Guild == null) | ||||
|                 reactions = _service.GlobalReactions; | ||||
|             else | ||||
|             { | ||||
|                 _service.GuildReactions.TryGetValue(Context.Guild.Id, out reactions); | ||||
|             } | ||||
|             if (reactions.Any()) | ||||
|             { | ||||
|                 var reaction = reactions.FirstOrDefault(x => x.Id == id); | ||||
|  | ||||
|                 if (reaction == null) | ||||
|                 { | ||||
|                     await ReplyErrorLocalized("no_found_id").ConfigureAwait(false); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 var setValue = reaction.AutoDeleteTrigger = !reaction.AutoDeleteTrigger; | ||||
|                  | ||||
|                 await _service.SetCrAdAsync(reaction.Id, setValue).ConfigureAwait(false); | ||||
|  | ||||
|                 if (setValue) | ||||
|                 { | ||||
|                     await ReplyConfirmLocalized("crad_enabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     await ReplyConfirmLocalized("crad_disabled", Format.Code(reaction.Id.ToString())).ConfigureAwait(false); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 await ReplyErrorLocalized("no_found").ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         [OwnerOnly] | ||||
|         public async Task CrStatsClear(string trigger = null) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(trigger)) | ||||
|             { | ||||
|                 _service.ClearStats(); | ||||
|                 await ReplyConfirmLocalized("all_stats_cleared").ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (_service.ReactionStats.TryRemove(trigger, out _)) | ||||
|                 { | ||||
|                     await ReplyErrorLocalized("stats_cleared", Format.Bold(trigger)).ConfigureAwait(false); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     await ReplyErrorLocalized("stats_not_found").ConfigureAwait(false); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [NadekoCommand, Usage, Description, Aliases] | ||||
|         public async Task CrStats(int page = 1) | ||||
|         { | ||||
|             if (--page < 0) | ||||
|                 return; | ||||
|             var ordered = _service.ReactionStats.OrderByDescending(x => x.Value).ToArray(); | ||||
|             if (!ordered.Any()) | ||||
|                 return; | ||||
|             var lastPage = ordered.Length / 9; | ||||
|             await Context.Channel.SendPaginatedConfirmAsync(_client, page, | ||||
|                 (curPage) => ordered.Skip(curPage * 9) | ||||
|                                     .Take(9) | ||||
|                                     .Aggregate(new EmbedBuilder().WithOkColor().WithTitle(GetText("stats")), | ||||
|                                             (agg, cur) => agg.AddField(efb => efb.WithName(cur.Key).WithValue(cur.Value.ToString()).WithIsInline(true))), lastPage) | ||||
|                 .ConfigureAwait(false); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										148
									
								
								NadekoBot.Core/Modules/CustomReactions/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								NadekoBot.Core/Modules/CustomReactions/Extensions/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| using AngleSharp; | ||||
| using AngleSharp.Dom.Html; | ||||
| using Discord; | ||||
| using Discord.WebSocket; | ||||
| using NadekoBot.Extensions; | ||||
| using NadekoBot.Modules.CustomReactions.Services; | ||||
| using NadekoBot.Services.Database.Models; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text.RegularExpressions; | ||||
| using System.Threading.Tasks; | ||||
| using NadekoBot.Common; | ||||
| using NadekoBot.Common.Replacements; | ||||
|  | ||||
| namespace NadekoBot.Modules.CustomReactions.Extensions | ||||
| { | ||||
|     public static class Extensions | ||||
|     { | ||||
|         private static readonly Regex imgRegex = new Regex("%(img|image):(?<tag>.*?)%", RegexOptions.Compiled); | ||||
|  | ||||
|         private static readonly NadekoRandom rng = new NadekoRandom(); | ||||
|  | ||||
|         public static Dictionary<Regex, Func<Match, Task<string>>> regexPlaceholders = new Dictionary<Regex, Func<Match, Task<string>>>() | ||||
|         { | ||||
|             { imgRegex, async (match) => { | ||||
|                 var tag = match.Groups["tag"].ToString(); | ||||
|                 if(string.IsNullOrWhiteSpace(tag)) | ||||
|                     return ""; | ||||
|  | ||||
|                 var fullQueryLink = $"http://imgur.com/search?q={ tag }"; | ||||
|                 var config = Configuration.Default.WithDefaultLoader(); | ||||
|                 var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink); | ||||
|  | ||||
|                 var elems = document.QuerySelectorAll("a.image-list-link").ToArray(); | ||||
|  | ||||
|                 if (!elems.Any()) | ||||
|                     return ""; | ||||
|  | ||||
|                 var img = (elems.ElementAtOrDefault(new NadekoRandom().Next(0, elems.Length))?.Children?.FirstOrDefault() as IHtmlImageElement); | ||||
|  | ||||
|                 if (img?.Source == null) | ||||
|                     return ""; | ||||
|  | ||||
|                 return " " + img.Source.Replace("b.", ".") + " "; | ||||
|             } } | ||||
|         }; | ||||
|  | ||||
|         private static string ResolveTriggerString(this string str, IUserMessage ctx, DiscordSocketClient client) | ||||
|         { | ||||
|             var rep = new ReplacementBuilder() | ||||
|                 .WithUser(ctx.Author) | ||||
|                 .WithClient(client) | ||||
|                 .Build(); | ||||
|  | ||||
|             str = rep.Replace(str.ToLowerInvariant()); | ||||
|  | ||||
|             return str; | ||||
|         } | ||||
|  | ||||
|         private static async Task<string> ResolveResponseStringAsync(this string str, IUserMessage ctx, DiscordSocketClient client, string resolvedTrigger, bool containsAnywhere) | ||||
|         { | ||||
|             var substringIndex = resolvedTrigger.Length; | ||||
|             if (containsAnywhere) | ||||
|             { | ||||
|                 var pos = ctx.Content.GetWordPosition(resolvedTrigger); | ||||
|                 if (pos == WordPosition.Start) | ||||
|                     substringIndex += 1; | ||||
|                 else if (pos == WordPosition.End) | ||||
|                     substringIndex = ctx.Content.Length; | ||||
|                 else if (pos == WordPosition.Middle) | ||||
|                     substringIndex += ctx.Content.IndexOf(resolvedTrigger); | ||||
|             } | ||||
|  | ||||
|             var rep = new ReplacementBuilder() | ||||
|                 .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) | ||||
|                 .WithOverride("%target%", () => ctx.Content.Substring(substringIndex).Trim()) | ||||
|                 .Build(); | ||||
|  | ||||
|             str = rep.Replace(str); | ||||
|  | ||||
|             foreach (var ph in regexPlaceholders) | ||||
|             { | ||||
|                 str = await ph.Key.ReplaceAsync(str, ph.Value); | ||||
|             } | ||||
|             return str; | ||||
|         } | ||||
|  | ||||
|         public static string TriggerWithContext(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client) | ||||
|             => cr.Trigger.ResolveTriggerString(ctx, client); | ||||
|  | ||||
|         public static Task<string > ResponseWithContextAsync(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, bool containsAnywhere) | ||||
|             => cr.Response.ResolveResponseStringAsync(ctx, client, cr.Trigger.ResolveTriggerString(ctx, client), containsAnywhere); | ||||
|  | ||||
|         public static async Task<IUserMessage> Send(this CustomReaction cr, IUserMessage ctx, DiscordSocketClient client, CustomReactionsService crs) | ||||
|         { | ||||
|             var channel = cr.DmResponse ? await ctx.Author.GetOrCreateDMChannelAsync() : ctx.Channel; | ||||
|  | ||||
|             crs.ReactionStats.AddOrUpdate(cr.Trigger, 1, (k, old) => ++old); | ||||
|  | ||||
|             if (CREmbed.TryParse(cr.Response, out CREmbed crembed)) | ||||
|             { | ||||
|                 var trigger = cr.Trigger.ResolveTriggerString(ctx, client); | ||||
|                 var substringIndex = trigger.Length; | ||||
|                 if (cr.ContainsAnywhere) | ||||
|                 { | ||||
|                     var pos = ctx.Content.GetWordPosition(trigger); | ||||
|                     if (pos == WordPosition.Start) | ||||
|                         substringIndex += 1; | ||||
|                     else if (pos == WordPosition.End) | ||||
|                         substringIndex = ctx.Content.Length; | ||||
|                     else if (pos == WordPosition.Middle) | ||||
|                         substringIndex += ctx.Content.IndexOf(trigger); | ||||
|                 } | ||||
|  | ||||
|                 var rep = new ReplacementBuilder() | ||||
|                     .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild, client) | ||||
|                     .WithOverride("%target%", () => ctx.Content.Substring(substringIndex).Trim()) | ||||
|                     .Build(); | ||||
|  | ||||
|                 rep.Replace(crembed); | ||||
|  | ||||
|                 return await channel.EmbedAsync(crembed.ToEmbed(), crembed.PlainText?.SanitizeMentions() ?? ""); | ||||
|             } | ||||
|             return await channel.SendMessageAsync((await cr.ResponseWithContextAsync(ctx, client, cr.ContainsAnywhere)).SanitizeMentions()); | ||||
|         } | ||||
|  | ||||
|         public static WordPosition GetWordPosition(this string str, string word) | ||||
|         { | ||||
|             if (str.StartsWith(word + " ")) | ||||
|                 return WordPosition.Start; | ||||
|             else if (str.EndsWith(" " + word)) | ||||
|                 return WordPosition.End; | ||||
|             else if (str.Contains(" " + word + " ")) | ||||
|                 return WordPosition.Middle; | ||||
|             else | ||||
|                 return WordPosition.None; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public enum WordPosition | ||||
|     { | ||||
|         None, | ||||
|         Start, | ||||
|         Middle, | ||||
|         End, | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,262 @@ | ||||
| using Discord; | ||||
| using Discord.WebSocket; | ||||
| using NadekoBot.Services.Database.Models; | ||||
| using NLog; | ||||
| using System.Collections.Concurrent; | ||||
| using System.Linq; | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using NadekoBot.Common; | ||||
| using NadekoBot.Common.ModuleBehaviors; | ||||
| using NadekoBot.Extensions; | ||||
| using NadekoBot.Services.Database; | ||||
| using NadekoBot.Services; | ||||
| using NadekoBot.Modules.CustomReactions.Extensions; | ||||
| using NadekoBot.Modules.Permissions.Common; | ||||
| using NadekoBot.Modules.Permissions.Services; | ||||
| using NadekoBot.Services.Impl; | ||||
| using Newtonsoft.Json; | ||||
|  | ||||
| namespace NadekoBot.Modules.CustomReactions.Services | ||||
| { | ||||
|     public class CustomReactionsService : IEarlyBlockingExecutor, INService | ||||
|     { | ||||
|         public CustomReaction[] GlobalReactions = new CustomReaction[] { }; | ||||
|         public ConcurrentDictionary<ulong, CustomReaction[]> GuildReactions { get; } = new ConcurrentDictionary<ulong, CustomReaction[]>(); | ||||
|  | ||||
|         public ConcurrentDictionary<string, uint> ReactionStats { get; } = new ConcurrentDictionary<string, uint>(); | ||||
|  | ||||
|         private readonly Logger _log; | ||||
|         private readonly DbService _db; | ||||
|         private readonly DiscordSocketClient _client; | ||||
|         private readonly PermissionService _perms; | ||||
|         private readonly CommandHandler _cmd; | ||||
|         private readonly IBotConfigProvider _bc; | ||||
|         private readonly NadekoStrings _strings; | ||||
|         private readonly IDataCache _cache; | ||||
|  | ||||
|         public CustomReactionsService(PermissionService perms, DbService db, NadekoStrings strings, | ||||
|             DiscordSocketClient client, CommandHandler cmd, IBotConfigProvider bc, IUnitOfWork uow,  | ||||
|             IDataCache cache) | ||||
|         { | ||||
|             _log = LogManager.GetCurrentClassLogger(); | ||||
|             _db = db; | ||||
|             _client = client; | ||||
|             _perms = perms; | ||||
|             _cmd = cmd; | ||||
|             _bc = bc; | ||||
|             _strings = strings; | ||||
|             _cache = cache; | ||||
|  | ||||
|             var sub = _cache.Redis.GetSubscriber(); | ||||
|             sub.Subscribe(_client.CurrentUser.Id + "_gcr.added", (ch, msg) => | ||||
|             { | ||||
|                 Array.Resize(ref GlobalReactions, GlobalReactions.Length + 1); | ||||
|                 GlobalReactions[GlobalReactions.Length - 1] = JsonConvert.DeserializeObject<CustomReaction>(msg); | ||||
|             }, StackExchange.Redis.CommandFlags.FireAndForget); | ||||
|             sub.Subscribe(_client.CurrentUser.Id + "_gcr.deleted", (ch, msg) => | ||||
|             { | ||||
|                 var id = int.Parse(msg); | ||||
|                 GlobalReactions = GlobalReactions.Where(cr => cr?.Id != id).ToArray(); | ||||
|             }, StackExchange.Redis.CommandFlags.FireAndForget); | ||||
|             sub.Subscribe(_client.CurrentUser.Id + "_gcr.edited", (ch, msg) => | ||||
|             { | ||||
|                 var obj = new { Id = 0, Message = "" }; | ||||
|                 obj = JsonConvert.DeserializeAnonymousType(msg, obj); | ||||
|                 var gcr = GlobalReactions.FirstOrDefault(x => x.Id == obj.Id); | ||||
|                 if (gcr != null) | ||||
|                     gcr.Response = obj.Message; | ||||
|             }, StackExchange.Redis.CommandFlags.FireAndForget); | ||||
|             sub.Subscribe(_client.CurrentUser.Id + "_crad.toggle", (ch, msg) => | ||||
|             { | ||||
|                 var obj = new { Id = 0, Value = false }; | ||||
|                 obj = JsonConvert.DeserializeAnonymousType(msg, obj); | ||||
|                 var gcr = GlobalReactions.FirstOrDefault(x => x.Id == obj.Id); | ||||
|                 if (gcr != null) | ||||
|                     gcr.AutoDeleteTrigger = obj.Value; | ||||
|             }, StackExchange.Redis.CommandFlags.FireAndForget); | ||||
|             sub.Subscribe(_client.CurrentUser.Id + "_crdm.toggle", (ch, msg) => | ||||
|             { | ||||
|                 var obj = new { Id = 0, Value = false }; | ||||
|                 obj = JsonConvert.DeserializeAnonymousType(msg, obj); | ||||
|                 var gcr = GlobalReactions.FirstOrDefault(x => x.Id == obj.Id); | ||||
|                 if(gcr != null) | ||||
|                     gcr.DmResponse = obj.Value; | ||||
|             }, StackExchange.Redis.CommandFlags.FireAndForget); | ||||
|             sub.Subscribe(_client.CurrentUser.Id + "_crca.toggle", (ch, msg) => | ||||
|             { | ||||
|                 var obj = new { Id = 0, Value = false }; | ||||
|                 obj = JsonConvert.DeserializeAnonymousType(msg, obj); | ||||
|                 var gcr = GlobalReactions.FirstOrDefault(x => x.Id == obj.Id); | ||||
|                 if (gcr != null) | ||||
|                     gcr.ContainsAnywhere = obj.Value; | ||||
|             }, StackExchange.Redis.CommandFlags.FireAndForget); | ||||
|  | ||||
|  | ||||
|             var items = uow.CustomReactions.GetAll(); | ||||
|  | ||||
|             GuildReactions = new ConcurrentDictionary<ulong, CustomReaction[]>(items.Where(g => g.GuildId != null && g.GuildId != 0).GroupBy(k => k.GuildId.Value).ToDictionary(g => g.Key, g => g.ToArray())); | ||||
|             GlobalReactions = items.Where(g => g.GuildId == null || g.GuildId == 0).ToArray(); | ||||
|         } | ||||
|  | ||||
|         public Task EditGcr(int id, string message) | ||||
|         { | ||||
|             var sub = _cache.Redis.GetSubscriber(); | ||||
|  | ||||
|             return sub.PublishAsync(_client.CurrentUser.Id + "_gcr.edited", JsonConvert.SerializeObject(new | ||||
|             { | ||||
|                 Id = id, | ||||
|                 Message = message, | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|         public Task AddGcr(CustomReaction cr) | ||||
|         { | ||||
|             var sub = _cache.Redis.GetSubscriber(); | ||||
|             return sub.PublishAsync(_client.CurrentUser.Id + "_gcr.added", JsonConvert.SerializeObject(cr)); | ||||
|         } | ||||
|  | ||||
|         public Task DelGcr(int id) | ||||
|         { | ||||
|             var sub = _cache.Redis.GetSubscriber(); | ||||
|             return sub.PublishAsync(_client.CurrentUser.Id + "_gcr.deleted", id); | ||||
|         } | ||||
|  | ||||
|         public void ClearStats() => ReactionStats.Clear(); | ||||
|  | ||||
|         public CustomReaction TryGetCustomReaction(IUserMessage umsg) | ||||
|         { | ||||
|             var channel = umsg.Channel as SocketTextChannel; | ||||
|             if (channel == null) | ||||
|                 return null; | ||||
|  | ||||
|             var content = umsg.Content.Trim().ToLowerInvariant(); | ||||
|  | ||||
|             if (GuildReactions.TryGetValue(channel.Guild.Id, out CustomReaction[] reactions)) | ||||
|                 if (reactions != null && reactions.Any()) | ||||
|                 { | ||||
|                     var rs = reactions.Where(cr => | ||||
|                     { | ||||
|                         if (cr == null) | ||||
|                             return false; | ||||
|  | ||||
|                         var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); | ||||
|                         var trigger = cr.TriggerWithContext(umsg, _client).Trim().ToLowerInvariant(); | ||||
|                         return ((cr.ContainsAnywhere &&  | ||||
|                             (content.GetWordPosition(trigger) != WordPosition.None)) | ||||
|                             || (hasTarget && content.StartsWith(trigger + " "))  | ||||
|                             || (_bc.BotConfig.CustomReactionsStartWith && content.StartsWith(trigger + " "))   | ||||
|                             || content == trigger); | ||||
|                     }).ToArray(); | ||||
|  | ||||
|                     if (rs.Length != 0) | ||||
|                     { | ||||
|                         var reaction = rs[new NadekoRandom().Next(0, rs.Length)]; | ||||
|                         if (reaction != null) | ||||
|                         { | ||||
|                             if (reaction.Response == "-") | ||||
|                                 return null; | ||||
|                             return reaction; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|             var grs = GlobalReactions.Where(cr => | ||||
|             { | ||||
|                 if (cr == null) | ||||
|                     return false; | ||||
|                 var hasTarget = cr.Response.ToLowerInvariant().Contains("%target%"); | ||||
|                 var trigger = cr.TriggerWithContext(umsg, _client).Trim().ToLowerInvariant(); | ||||
|                 return ((cr.ContainsAnywhere && | ||||
|                             (content.GetWordPosition(trigger) != WordPosition.None))  | ||||
|                         || (hasTarget && content.StartsWith(trigger + " "))  | ||||
|                         || (_bc.BotConfig.CustomReactionsStartWith && content.StartsWith(trigger + " "))  | ||||
|                         || content == trigger); | ||||
|             }).ToArray(); | ||||
|             if (grs.Length == 0) | ||||
|                 return null; | ||||
|             var greaction = grs[new NadekoRandom().Next(0, grs.Length)]; | ||||
|  | ||||
|             return greaction; | ||||
|         } | ||||
|  | ||||
|         public async Task<bool> TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage msg) | ||||
|         { | ||||
|             // maybe this message is a custom reaction | ||||
|             var cr = await Task.Run(() => TryGetCustomReaction(msg)).ConfigureAwait(false); | ||||
|             if (cr != null) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     if (guild is SocketGuild sg) | ||||
|                     { | ||||
|                         var pc = _perms.GetCache(guild.Id); | ||||
|                         if (!pc.Permissions.CheckPermissions(msg, cr.Trigger, "ActualCustomReactions", | ||||
|                             out int index)) | ||||
|                         { | ||||
|                             if (pc.Verbose) | ||||
|                             { | ||||
|                                 var returnMsg = _strings.GetText("trigger", guild.Id, "Permissions".ToLowerInvariant(), index + 1, Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))); | ||||
|                                 try { await msg.Channel.SendErrorAsync(returnMsg).ConfigureAwait(false); } catch { } | ||||
|                                 _log.Info(returnMsg); | ||||
|                             } | ||||
|                             return true; | ||||
|                         } | ||||
|                     } | ||||
|                     await cr.Send(msg, _client, this).ConfigureAwait(false); | ||||
|  | ||||
|                     if (cr.AutoDeleteTrigger) | ||||
|                     { | ||||
|                         try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } | ||||
|                     } | ||||
|                     return true; | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     _log.Warn("Sending CREmbed failed"); | ||||
|                     _log.Warn(ex); | ||||
|                 } | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         public Task SetCrDmAsync(int id, bool setValue) | ||||
|         { | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 uow.CustomReactions.Get(id).DmResponse = setValue; | ||||
|                 uow.Complete(); | ||||
|             } | ||||
|  | ||||
|             var sub = _cache.Redis.GetSubscriber(); | ||||
|             var data = new { Id = id, Value = setValue }; | ||||
|             return sub.PublishAsync(_client.CurrentUser.Id + "_crdm.toggle", JsonConvert.SerializeObject(data)); | ||||
|         } | ||||
|  | ||||
|         public Task SetCrAdAsync(int id, bool setValue) | ||||
|         { | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 uow.CustomReactions.Get(id).AutoDeleteTrigger = setValue; | ||||
|                 uow.Complete(); | ||||
|             } | ||||
|  | ||||
|             var sub = _cache.Redis.GetSubscriber(); | ||||
|             var data = new { Id = id, Value = setValue }; | ||||
|             return sub.PublishAsync(_client.CurrentUser.Id + "_crad.toggle", JsonConvert.SerializeObject(data)); | ||||
|         } | ||||
|  | ||||
|         public Task SetCrCaAsync(int id, bool setValue) | ||||
|         { | ||||
|             using (var uow = _db.UnitOfWork) | ||||
|             { | ||||
|                 uow.CustomReactions.Get(id).ContainsAnywhere = setValue; | ||||
|                 uow.Complete(); | ||||
|             } | ||||
|  | ||||
|             var sub = _cache.Redis.GetSubscriber(); | ||||
|             var data = new { Id = id, Value = setValue }; | ||||
|             return sub.PublishAsync(_client.CurrentUser.Id + "_crca.toggle", JsonConvert.SerializeObject(data)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user