diff --git a/Discord.Addons.Interactive/InlineReaction/InlineReactionCallback.cs b/Discord.Addons.Interactive/InlineReaction/InlineReactionCallback.cs new file mode 100644 index 0000000..d446061 --- /dev/null +++ b/Discord.Addons.Interactive/InlineReaction/InlineReactionCallback.cs @@ -0,0 +1,66 @@ +using Discord.Commands; +using Discord.WebSocket; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord.Addons.Interactive +{ + public class InlineReactionCallback : IReactionCallback + { + public RunMode RunMode => RunMode.Sync; + + public ICriterion Criterion { get; } + + public TimeSpan? Timeout { get; } + + public SocketCommandContext Context { get; } + + public IUserMessage Message { get; private set; } + + private readonly InteractiveService _interactive; + private readonly ReactionCallbackData _data; + + public InlineReactionCallback( + InteractiveService interactive, + SocketCommandContext context, + ReactionCallbackData data, + ICriterion criterion = null) + { + _interactive = interactive; + Context = context; + _data = data; + Criterion = criterion ?? new EmptyCriterion(); + Timeout = data.Timeout ?? TimeSpan.FromSeconds(30); + } + + public async Task DisplayAsync() + { + var message = await Context.Channel.SendMessageAsync(_data.Text, embed: _data.Embed).ConfigureAwait(false); + Message = message; + _interactive.AddReactionCallback(message, this); + + _ = Task.Run(async () => + { + foreach (var item in _data.Callbacks) + await message.AddReactionAsync(item.Reaction); + }); + + if (Timeout.HasValue) + { + _ = Task.Delay(Timeout.Value) + .ContinueWith(_ => _interactive.RemoveReactionCallback(message)); + } + } + + public async Task HandleCallbackAsync(SocketReaction reaction) + { + var reactionCallbackItem = _data.Callbacks.FirstOrDefault(t => t.Reaction.Equals(reaction.Emote)); + if (reactionCallbackItem == null) + return false; + + await reactionCallbackItem.Callback(Context); + return true; + } + } +} diff --git a/Discord.Addons.Interactive/InlineReaction/ReactionCallbackData.cs b/Discord.Addons.Interactive/InlineReaction/ReactionCallbackData.cs new file mode 100644 index 0000000..77dcf84 --- /dev/null +++ b/Discord.Addons.Interactive/InlineReaction/ReactionCallbackData.cs @@ -0,0 +1,32 @@ +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Addons.Interactive +{ + public class ReactionCallbackData + { + private readonly ICollection _items; + + public string Text { get; } + public Embed Embed { get; } + public TimeSpan? Timeout { get; } + public IEnumerable Callbacks => _items; + + public ReactionCallbackData(string text, Embed embed = null, TimeSpan? timeout = null) + { + Text = text; + Embed = embed; + Timeout = timeout; + _items = new List(); + } + + public ReactionCallbackData WithCallback(IEmote reaction, Func callback) + { + var item = new ReactionCallbackItem(reaction, callback); + _items.Add(item); + return this; + } + } +} diff --git a/Discord.Addons.Interactive/InlineReaction/ReactionCallbackItem.cs b/Discord.Addons.Interactive/InlineReaction/ReactionCallbackItem.cs new file mode 100644 index 0000000..4c224fe --- /dev/null +++ b/Discord.Addons.Interactive/InlineReaction/ReactionCallbackItem.cs @@ -0,0 +1,18 @@ +using Discord.Commands; +using System; +using System.Threading.Tasks; + +namespace Discord.Addons.Interactive +{ + public class ReactionCallbackItem + { + public IEmote Reaction { get; } + public Func Callback { get; } + + public ReactionCallbackItem(IEmote reaction, Func callback) + { + Reaction = reaction; + Callback = callback; + } + } +} diff --git a/Discord.Addons.Interactive/InteractiveBase.cs b/Discord.Addons.Interactive/InteractiveBase.cs index c934f85..9349384 100644 --- a/Discord.Addons.Interactive/InteractiveBase.cs +++ b/Discord.Addons.Interactive/InteractiveBase.cs @@ -41,6 +41,9 @@ public Task PagedReplyAsync(PaginatedMessage pager, bool fromSourc public Task PagedReplyAsync(PaginatedMessage pager, ICriterion criterion) => Interactive.SendPaginatedMessageAsync(Context, pager, criterion); + public Task InlineReactionReplyAsync(ReactionCallbackData data, bool fromSourceUser = true) + => Interactive.SendMessageWithReactionCallbacksAsync(Context, data, fromSourceUser); + public RuntimeResult Ok(string reason = null) => new OkResult(reason); } } diff --git a/Discord.Addons.Interactive/InteractiveService.cs b/Discord.Addons.Interactive/InteractiveService.cs index 11977f0..abb644a 100644 --- a/Discord.Addons.Interactive/InteractiveService.cs +++ b/Discord.Addons.Interactive/InteractiveService.cs @@ -3,7 +3,6 @@ using Discord.Commands; using Discord.WebSocket; using System.Collections.Generic; -using System.Linq; namespace Discord.Addons.Interactive { @@ -46,7 +45,7 @@ async Task Handler(SocketMessage message) } context.Client.MessageReceived += Handler; - + var trigger = eventTrigger.Task; var delay = Task.Delay(timeout.Value); var task = await Task.WhenAny(trigger, delay).ConfigureAwait(false); @@ -76,6 +75,16 @@ public async Task SendPaginatedMessageAsync(SocketCommandContext c return callback.Message; } + public async Task SendMessageWithReactionCallbacksAsync(SocketCommandContext context, ReactionCallbackData callbacks, bool fromSourceUser = true) + { + var criterion = new Criteria(); + if(fromSourceUser) + criterion.AddCriterion(new EnsureReactionFromSourceUserCriterion()); + var callback = new InlineReactionCallback(this, context, callbacks, criterion); + await callback.DisplayAsync().ConfigureAwait(false); + return callback.Message; + } + public void AddReactionCallback(IMessage message, IReactionCallback callback) => _callbacks[message.Id] = callback; public void RemoveReactionCallback(IMessage message) @@ -84,7 +93,7 @@ public void RemoveReactionCallback(ulong id) => _callbacks.Remove(id); public void ClearReactionCallbacks() => _callbacks.Clear(); - + private async Task HandleReactionAsync(Cacheable message, ISocketMessageChannel channel, SocketReaction reaction) { if (reaction.UserId == Discord.CurrentUser.Id) return; diff --git a/SampleApp/Modules/SampleModule.cs b/SampleApp/Modules/SampleModule.cs index 4436e5b..a6db863 100644 --- a/SampleApp/Modules/SampleModule.cs +++ b/SampleApp/Modules/SampleModule.cs @@ -1,7 +1,8 @@ -using System; -using System.Threading.Tasks; +using Discord; using Discord.Addons.Interactive; using Discord.Commands; +using System; +using System.Threading.Tasks; namespace SampleApp.Modules { @@ -40,5 +41,35 @@ public async Task Test_Paginator() var pages = new[] { "Page 1", "Page 2", "Page 3", "aaaaaa", "Page 5" }; await PagedReplyAsync(pages); } + + // InlineReactionReplyAsync will send a message and adds reactions on it. + // Once an user adds a reaction, the callback is fired. + // If callback was successfull next callback is not handled (message is unsubscribed). + // Unsuccessful callback is a reaction that did not have a callback. + [Command("reaction")] + public async Task Test_ReactionReply() + { + await InlineReactionReplyAsync(new ReactionCallbackData("text") + .WithCallback(new Emoji("👍"), c => c.Channel.SendMessageAsync("You've replied with 👍")) + .WithCallback(new Emoji("👎"), c => c.Channel.SendMessageAsync("You've replied with 👎")) + ); + } + [Command("embedreaction")] + public async Task Test_EmedReactionReply() + { + var one = new Emoji("1⃣"); + var two = new Emoji("2⃣"); + + var embed = new EmbedBuilder() + .WithTitle("Choose one") + .AddInlineField(one.Name, "Beer") + .AddInlineField(two.Name, "Drink") + .Build(); + + await InlineReactionReplyAsync(new ReactionCallbackData("text", embed) + .WithCallback(one, c => c.Channel.SendMessageAsync("Here you go :beer:")) + .WithCallback(two, c => c.Channel.SendMessageAsync("Here you go :tropical_drink:")) + ); + } } } diff --git a/SampleApp/SampleApp.csproj b/SampleApp/SampleApp.csproj index 208d140..c86d83a 100644 --- a/SampleApp/SampleApp.csproj +++ b/SampleApp/SampleApp.csproj @@ -6,8 +6,8 @@ - - + +