Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change reaction's handling on poll messages #340

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions src/HonzaBotner.Discord.Services/Commands/PollCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ public class PollCommands : BaseCommandModule

private readonly CommonCommandOptions _options;
private readonly ILogger<PollCommands> _logger;
private readonly IGuildProvider _guildProvider;

public PollCommands(IOptions<CommonCommandOptions> options, ILogger<PollCommands> logger)
public PollCommands(IOptions<CommonCommandOptions> options, ILogger<PollCommands> logger,
IGuildProvider guildProvider)
{
_options = options.Value;
_logger = logger;
_guildProvider = guildProvider;
}

[GroupCommand]
Expand Down Expand Up @@ -83,27 +86,27 @@ public async Task AbcPollCommandAsync(

private async Task CreateDefaultPollAsync(CommandContext ctx, string question, List<string>? answers = null)
{
Poll poll = answers is null
? new YesNoPoll(ctx.Member.Mention, question)
: new AbcPoll(ctx.Member.Mention, question, answers);

try
{
Poll poll = answers is null
? new YesNoPoll(ctx.Member.Mention, question)
: new AbcPoll(ctx.Member.Mention, question, answers);

await poll.PostAsync(ctx.Client, ctx.Channel);
await ctx.Message.DeleteAsync();
}
catch (ArgumentException e)
catch (PollException e)
{
await ctx.RespondAsync(e.Message);
}
catch (Exception e)
{
await ctx.RespondAsync(PollErrorMessage);
_logger.LogWarning(e, "Failed to create new {PollType}", poll.PollType);
_logger.LogWarning(e, "Failed to create new Poll");
}
}

private async Task PollHelpAsync(CommandContext ctx)
private static async Task PollHelpAsync(CommandContext ctx)
{
DiscordEmbed embed = new DiscordEmbedBuilder()
.WithTitle("Polls")
Expand Down Expand Up @@ -146,21 +149,23 @@ public async Task AddPollOptionAsync(
return;
}

DiscordRole modRole = (await ctx.Client.GetGuildAsync(ctx.Guild.Id)).GetRole(_options.ModRoleId);
AbcPoll poll = new(originalMessage);

if (poll.AuthorMention != ctx.Member.Mention && !ctx.Member.Roles.Contains(modRole))
{
await ctx.RespondAsync("You are not authorized to edit this poll");
return;
}

try
{
await new AbcPoll(originalMessage).AddOptionsAsync(ctx.Client, options.ToList());
DiscordRole modRole = (await _guildProvider.GetCurrentGuildAsync()).GetRole(_options.ModRoleId);

// I am sorry, due to DSharpPlus' caching logic, this mess is necessary
AbcPoll poll = new (await (await ctx.Client.GetChannelAsync(ctx.Channel.Id)).GetMessageAsync(ctx.Message.ReferencedMessage.Id));
tenhobi marked this conversation as resolved.
Show resolved Hide resolved

if (poll.AuthorMention != ctx.Member?.Mention && !(ctx.Member?.Roles.Contains(modRole) ?? false))
{
await ctx.RespondAsync("You are not authorized to edit this poll");
return;
}

await poll.AddOptionsAsync(ctx.Client, options);
await ctx.Message.CreateReactionAsync(DiscordEmoji.FromName(ctx.Client, ":+1:"));
}
catch (ArgumentException e)
catch (PollException e)
{
await ctx.RespondAsync(e.Message);
}
Expand Down
64 changes: 62 additions & 2 deletions src/HonzaBotner.Discord.Services/Commands/Polls/AbcPoll.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Entities;
using HonzaBotner.Discord.Services.Extensions;

namespace HonzaBotner.Discord.Services.Commands.Polls;

public class AbcPoll : Poll
{
public override string PollType => "AbcPoll";

public override List<string> OptionsEmoji => new()
protected override List<string> OptionsEmoji => new()
{
":regional_indicator_a:",
":regional_indicator_b:",
Expand Down Expand Up @@ -35,4 +40,59 @@ public AbcPoll(string authorMention, string question, List<string> options)
public AbcPoll(DiscordMessage message) : base(message)
{
}

public async Task AddOptionsAsync(DiscordClient client, IEnumerable<string> newOptions)
{
const int reactionCap = 20; // Max amount of reactions present on a message, lower than Discord provided, in case some trolls block bot's reactions
if (ExistingPollMessage is null)
{
throw new InvalidOperationException("You can edit only poll constructed from sent message.");
}

NewChoices = newOptions.ToList();

List<string> emojisToAdd = OptionsEmoji;

// Look at existing message and allow only emojis which are not yet present on that message.
emojisToAdd.RemoveAll(emoji =>
ExistingPollMessage.Reactions.Select(rect => rect.Emoji).Contains(DiscordEmoji.FromName(client, emoji)));

emojisToAdd = emojisToAdd.GetRange(
0,
// Allow only so many reactions, that we don't cross 20 reactions on existing message
Math.Min(Math.Min(reactionCap - Math.Min(ExistingPollMessage.Reactions.Count, reactionCap),
// Take above reaction capacity, and lower it optionally to number of emojis which we are able to react with
emojisToAdd.Count),
// Take the above number and cap it at total new choices we want to add (can be lower or equal to real choices number)
NewChoices.Count));

// The new options count will be equal or lower than total options added, based on available emojis
NewChoices = NewChoices.GetRange(0, emojisToAdd.Count);

if (NewChoices.Count == 0)
{
throw new PollException($"Total number of reactions on a message can't be greater than {reactionCap}");
}
await ExistingPollMessage
.ModifyAsync(Modify(client, ExistingPollMessage.Channel.Guild, ExistingPollMessage.Embeds[0], emojisToAdd));

Task _ = Task.Run(async () => { await AddReactionsAsync(client, ExistingPollMessage, emojisToAdd); });
}

private DiscordEmbed Modify(DiscordClient client, DiscordGuild guild, DiscordEmbed original, IEnumerable<string> emojisToAdd)
{
DiscordEmbedBuilder builder = new (original);

NewChoices.Zip(emojisToAdd).ToList().ForEach(pair =>
{
(string? answer, string? emojiName) = pair;

builder.AddField(
DiscordEmoji.FromName(client, emojiName).ToString(),
answer.RemoveDiscordMentions(guild),
true);
});

return builder.WithFooter(PollType).Build();
}
}
54 changes: 18 additions & 36 deletions src/HonzaBotner.Discord.Services/Commands/Polls/Poll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,37 @@ namespace HonzaBotner.Discord.Services.Commands.Polls;

public abstract class Poll
{
public abstract List<string> OptionsEmoji { get; }
protected abstract List<string> OptionsEmoji { get; }
public abstract string PollType { get; }

public virtual List<string> ActiveEmojis
protected virtual List<string> UsedEmojis
{
get => OptionsEmoji.GetRange(0, _choices.Count);
get => OptionsEmoji.GetRange(0, NewChoices.Count);
}

protected List<string> NewChoices;

public readonly string AuthorMention;
private readonly List<string> _choices;
private readonly DiscordMessage? _existingPollMessage;
private readonly string _question;
protected readonly DiscordMessage? ExistingPollMessage;
protected readonly string Question;

protected Poll(string authorMention, string question, List<string>? options = null)
{
AuthorMention = authorMention;
_question = question;
_choices = options ?? new List<string>();
Question = question;
NewChoices = options ?? new List<string>();
}

protected Poll(DiscordMessage originalMessage)
{
_existingPollMessage = originalMessage;
DiscordEmbed originalPoll = _existingPollMessage.Embeds[0];
ExistingPollMessage = originalMessage;
DiscordEmbed originalPoll = ExistingPollMessage.Embeds[0];

// Extract original author Mention via discord's mention format <@!123456789>.
AuthorMention = originalPoll.Description.Substring(
originalPoll.Description.LastIndexOf("<", StringComparison.Ordinal)
);

_choices = originalPoll.Fields?
.Select(ef => ef.Value)
.ToList() ?? new List<string>();

_question = originalPoll.Title;
Question = originalPoll.Title;
}

public async Task PostAsync(DiscordClient client, DiscordChannel channel)
Expand All @@ -54,42 +50,28 @@ public async Task PostAsync(DiscordClient client, DiscordChannel channel)
Task _ = Task.Run(async () => { await AddReactionsAsync(client, pollMessage); });
}

public virtual async Task AddOptionsAsync(DiscordClient client, IEnumerable<string> newOptions)
{
if (_existingPollMessage == null)
{
throw new InvalidOperationException("You can edit only poll constructed from sent message.");
}

_choices.AddRange(newOptions);

await _existingPollMessage.ModifyAsync(Build(client, _existingPollMessage.Channel.Guild));

Task _ = Task.Run(async () => { await AddReactionsAsync(client, _existingPollMessage); });
}

protected async Task AddReactionsAsync(DiscordClient client, DiscordMessage message)
protected async Task AddReactionsAsync(DiscordClient client, DiscordMessage message, List<string>? reactions = null)
{
foreach (string reaction in ActiveEmojis)
foreach (string reaction in reactions ?? UsedEmojis)
{
await message.CreateReactionAsync(DiscordEmoji.FromName(client, reaction));
}
}

private DiscordEmbed Build(DiscordClient client, DiscordGuild guild)
{
if (_choices.Count > OptionsEmoji.Count)
if (NewChoices.Count > OptionsEmoji.Count)
{
throw new ArgumentException($"Too many options. Maximum options is {OptionsEmoji.Count}.");
throw new PollException($"Too many options. Maximum options is {OptionsEmoji.Count}.");
}

DiscordEmbedBuilder builder = new()
{
Title = _question.RemoveDiscordMentions(guild),
Title = Question.RemoveDiscordMentions(guild),
Description = "By: " + AuthorMention // Author needs to stay as the last argument
};

_choices.Zip(ActiveEmojis).ToList().ForEach(pair =>
NewChoices.Zip(UsedEmojis).ToList().ForEach(pair =>
{
(string? answer, string? emojiName) = pair;

Expand Down
17 changes: 17 additions & 0 deletions src/HonzaBotner.Discord.Services/Commands/Polls/PollException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace HonzaBotner.Discord.Services.Commands.Polls;

public class PollException : Exception
{
public PollException ()
{}

public PollException (string message)
: base(message)
{}

public PollException (string message, Exception innerException)
: base (message, innerException)
{}
}
16 changes: 7 additions & 9 deletions src/HonzaBotner.Discord.Services/Commands/Polls/YesNoPoll.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DSharpPlus;
using System.Collections.Generic;
using DSharpPlus.Entities;

namespace HonzaBotner.Discord.Services.Commands.Polls;

public class YesNoPoll : Poll
{
public override string PollType => "YesNoPoll";
public override List<string> OptionsEmoji => new() { ":+1:", ":-1:" };
public override List<string> ActiveEmojis => OptionsEmoji;
protected override List<string> OptionsEmoji => new() { ":+1:", ":-1:" };

protected override List<string> UsedEmojis
{
get => OptionsEmoji;
}

public YesNoPoll(string authorMention, string question) : base(authorMention, question)
{
Expand All @@ -19,7 +20,4 @@ public YesNoPoll(string authorMention, string question) : base(authorMention, qu
public YesNoPoll(DiscordMessage message) : base(message)
{
}

public override Task AddOptionsAsync(DiscordClient client, IEnumerable<string> newOptions) =>
throw new ArgumentException($"Adding options is disabled for {PollType}");
}

This file was deleted.

1 change: 0 additions & 1 deletion src/HonzaBotner/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public void ConfigureServices(IServiceCollection services)
.AddEventHandler<NewChannelHandler>()
.AddEventHandler<PinHandler>()
.AddEventHandler<ReminderReactionsHandler>()
.AddEventHandler<PollReactionsHandler>(EventHandlerPriority.Low)
.AddEventHandler<RoleBindingsHandler>(EventHandlerPriority.High)
.AddEventHandler<StaffVerificationEventHandler>(EventHandlerPriority.Urgent)
.AddEventHandler<VerificationEventHandler>(EventHandlerPriority.Urgent)
Expand Down