Created Poll Maker plugin
This commit is contained in:
32
Plugins/PollMaker/Internal/PollState.cs
Normal file
32
Plugins/PollMaker/Internal/PollState.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace PollMaker.Internal;
|
||||
|
||||
internal sealed record PollState(string Question, string[] Options)
|
||||
{
|
||||
public List<HashSet<ulong>> Votes { get; } =
|
||||
Enumerable.Range(0, Options.Length).Select(_ => new HashSet<ulong>()).ToList();
|
||||
|
||||
public bool IsOpen { get; private set; } = true;
|
||||
|
||||
public void Close() => IsOpen = false;
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the member’s vote.
|
||||
/// Clicking the **same button** again removes their vote;
|
||||
/// clicking a **different** button moves the vote.
|
||||
/// </summary>
|
||||
public void ToggleVote(int optionIdx, ulong userId)
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
|
||||
if (Votes[optionIdx].Contains(userId))
|
||||
{
|
||||
Votes[optionIdx].Remove(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var set in Votes)
|
||||
set.Remove(userId);
|
||||
|
||||
Votes[optionIdx].Add(userId);
|
||||
}
|
||||
}
|
||||
17
Plugins/PollMaker/PollMaker.csproj
Normal file
17
Plugins/PollMaker/PollMaker.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\DiscordBotCore.PluginCore\DiscordBotCore.PluginCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Commands\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
171
Plugins/PollMaker/SlashCommands/MakePoll.cs
Normal file
171
Plugins/PollMaker/SlashCommands/MakePoll.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using DiscordBotCore.Logging;
|
||||
using DiscordBotCore.PluginCore.Interfaces;
|
||||
using PollMaker.Internal;
|
||||
|
||||
namespace PollMaker.SlashCommands;
|
||||
|
||||
public class MakePoll : IDbSlashCommand
|
||||
{
|
||||
public string Name => "make-poll";
|
||||
public string Description => "Create an interactive poll (2-25 answers, optional timer)";
|
||||
public bool CanUseDm => false;
|
||||
public bool HasInteraction => true;
|
||||
|
||||
// ─────────── slash-command schema ───────────
|
||||
public List<SlashCommandOptionBuilder> Options =>
|
||||
[
|
||||
new SlashCommandOptionBuilder
|
||||
{
|
||||
Name = "question",
|
||||
Description = "The poll question",
|
||||
Type = ApplicationCommandOptionType.String,
|
||||
IsRequired = true
|
||||
},
|
||||
new SlashCommandOptionBuilder
|
||||
{
|
||||
Name = "answers",
|
||||
Description = "Answers separated with ';' (min 2, max 25)",
|
||||
Type = ApplicationCommandOptionType.String,
|
||||
IsRequired = true
|
||||
},
|
||||
new SlashCommandOptionBuilder
|
||||
{
|
||||
Name = "timed",
|
||||
Description = "Close the poll automatically after a given duration",
|
||||
Type = ApplicationCommandOptionType.Boolean,
|
||||
IsRequired = false
|
||||
},
|
||||
new SlashCommandOptionBuilder
|
||||
{
|
||||
Name = "duration",
|
||||
Description = "Duration in **hours** (1-168) – required if timed = true",
|
||||
Type = ApplicationCommandOptionType.Integer,
|
||||
MinValue = 1,
|
||||
MaxValue = 168,
|
||||
IsRequired = false
|
||||
}
|
||||
];
|
||||
|
||||
// ─────────── in-memory cache ───────────
|
||||
private static readonly ConcurrentDictionary<ulong, PollState> Polls = new();
|
||||
|
||||
// ─────────── slash-command handler ───────────
|
||||
public async void ExecuteServer(ILogger log, SocketSlashCommand ctx)
|
||||
{
|
||||
string q = ctx.Data.Options.First(o => o.Name == "question").Value!.ToString()!.Trim();
|
||||
string raw = ctx.Data.Options.First(o => o.Name == "answers" ).Value!.ToString()!;
|
||||
|
||||
bool timed = ctx.Data.Options.FirstOrDefault(o => o.Name == "timed")?.Value is bool b && b;
|
||||
int hours = ctx.Data.Options.FirstOrDefault(o => o.Name == "duration")?.Value is long l ? (int)l : 0;
|
||||
|
||||
if (timed && hours == 0)
|
||||
{
|
||||
await ctx.RespondAsync("❗ When `timed` is **true**, you must supply a `duration` (1-168 hours).",
|
||||
ephemeral: true);
|
||||
return;
|
||||
}
|
||||
|
||||
var opts = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(a => a.Trim())
|
||||
.Where(a => a.Length > 0)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (opts.Length < 2 || opts.Length > 25)
|
||||
{
|
||||
await ctx.RespondAsync($"❗ You must supply **2-25** answers; you supplied {opts.Length}.",
|
||||
ephemeral: true);
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle($"📊 {q}")
|
||||
.WithDescription(string.Join('\n', opts.Select((o,i) => $"{i+1}. {o}")))
|
||||
.WithColor(Color.Purple)
|
||||
.WithFooter(timed
|
||||
? $"Click a button to vote • click again to un-vote • closes in {hours} h"
|
||||
: "Click a button to vote • click again to un-vote")
|
||||
.Build();
|
||||
|
||||
var cb = new ComponentBuilder();
|
||||
for (int i = 0; i < opts.Length; i++)
|
||||
cb.WithButton(label: $"{i+1}",
|
||||
customId: $"poll:{ctx.Id}:{i}", // poll:{slashId}:{idx}
|
||||
style: ButtonStyle.Secondary,
|
||||
row: i / 5);
|
||||
|
||||
await ctx.RespondAsync(embed: embed, components: cb.Build());
|
||||
var msg = await ctx.GetOriginalResponseAsync();
|
||||
|
||||
var state = new PollState(q, opts);
|
||||
Polls[msg.Id] = state;
|
||||
|
||||
if (timed)
|
||||
_ = ClosePollLaterAsync(log, msg, state, hours);
|
||||
}
|
||||
|
||||
public async Task ExecuteInteraction(ILogger log, SocketInteraction interaction)
|
||||
{
|
||||
if (interaction is not SocketMessageComponent btn || !btn.Data.CustomId.StartsWith("poll:"))
|
||||
return;
|
||||
|
||||
if (!Polls.TryGetValue(btn.Message.Id, out var poll))
|
||||
{
|
||||
await btn.RespondAsync("This poll is no longer active.", ephemeral: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!poll.IsOpen)
|
||||
{
|
||||
await btn.RespondAsync("The poll has already closed.", ephemeral: true);
|
||||
return;
|
||||
}
|
||||
|
||||
var optionIdx = int.Parse(btn.Data.CustomId.Split(':')[2]);
|
||||
|
||||
poll.ToggleVote(optionIdx, btn.User.Id);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithTitle($"📊 {poll.Question}")
|
||||
.WithDescription(string.Join('\n',
|
||||
poll.Options.Select((o,i) => $"{i+1}. {o} — **{poll.Votes[i].Count}**")))
|
||||
.WithColor(Color.Purple)
|
||||
.WithFooter("Click a button to vote • click again to un-vote")
|
||||
.Build();
|
||||
|
||||
await btn.Message.ModifyAsync(m => m.Embed = embed);
|
||||
await btn.DeferAsync();
|
||||
}
|
||||
private static async Task ClosePollLaterAsync(ILogger log, IUserMessage msg, PollState poll, int hours)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromHours(hours));
|
||||
|
||||
poll.Close();
|
||||
|
||||
var closedEmbed = new EmbedBuilder()
|
||||
.WithTitle($"📊 {poll.Question} — closed")
|
||||
.WithDescription(string.Join('\n',
|
||||
poll.Options.Select((o,i) => $"{i+1}. {o} — **{poll.Votes[i].Count}**")))
|
||||
.WithColor(Color.DarkGrey)
|
||||
.WithFooter($"Poll closed after {hours} h • thanks for voting!")
|
||||
.Build();
|
||||
|
||||
await msg.ModifyAsync(m =>
|
||||
{
|
||||
m.Embed = closedEmbed;
|
||||
m.Components = new ComponentBuilder().Build();
|
||||
});
|
||||
|
||||
Polls.TryRemove(msg.Id, out _);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogException(ex, typeof(MakePoll));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotCore.Database.Sql
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUI", "WebUI\WebUI.csproj", "{DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PollMaker", "Plugins\PollMaker\PollMaker.csproj", "{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -201,6 +203,18 @@ Global
|
||||
{DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}.Release|x64.ActiveCfg = Release|x64
|
||||
{DE42253E-2ED6-4653-B9CC-C2C2551E1EA8}.Release|x64.Build.0 = Release|x64
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Debug|ARM64.ActiveCfg = Debug|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Debug|ARM64.Build.0 = Debug|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Release|ARM64.ActiveCfg = Release|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Release|ARM64.Build.0 = Release|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7}.Release|x64.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -209,6 +223,7 @@ Global
|
||||
{FCE9743F-7EB4-4639-A080-FCDDFCC7D689} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1}
|
||||
{F3C61A47-F758-4145-B496-E3ECCF979D89} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1}
|
||||
{C67908F9-4A55-4DD8-B993-C26C648226F1} = {EA4FA308-7B2C-458E-8485-8747D745DD59}
|
||||
{9A4B98C1-00AC-481C-BE55-A70C0B9D3BE7} = {5CF9AD7B-6BF0-4035-835F-722F989C01E1}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {3FB3C5DE-ED21-4D2E-ABDD-3A00EE4A2FFF}
|
||||
|
||||
Reference in New Issue
Block a user