Skip to content

Commit

Permalink
Add initial functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
zobweyt committed Jun 27, 2024
1 parent 27e4c21 commit 2f4cc66
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 0 deletions.
30 changes: 30 additions & 0 deletions src/Giveaways/Common/Args/GiveawayStartArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using Discord.Interactions;

namespace Giveaways;

[method: ComplexParameterCtor]
public class GiveawayStartArgs(
[MinLength(2)]
[MaxLength(128)]
[Summary(description: "The prize of the giveaway.")]
string prize,

[MinValue(1)]
[MaxValue(25)]
[Summary(description: "The maximum number of winners for the giveaway.")]
int winners,

[Choice("1 hour", "1h")]
[Choice("4 hours", "4h")]
[Choice("8 hours", "8h")]
[Choice("1 day", "1d")]
[Choice("3 days", "3d")]
[Choice("1 week", "7d")]
[Summary(description: "The duration of the giveaway.")]
TimeSpan duration)
{
public string Prize => prize;
public int MaxWinners => winners;
public DateTime ExpiresAt => DateTime.Now + duration;
}
113 changes: 113 additions & 0 deletions src/Giveaways/Modules/GiveawayModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.Interactions;
using Giveaways.Data;
using Giveaways.Services;
using Hangfire.States;
using Microsoft.EntityFrameworkCore;

namespace Giveaways.Modules;

[RequireContext(ContextType.Guild)]
public class GiveawayModule : ModuleBase
{
private readonly AppDbContext _db;
private readonly GiveawayFormatter _formatter;
private readonly GiveawayScheduler _scheduler;
private readonly GiveawayService _service;

public GiveawayModule(AppDbContext db, GiveawayFormatter formatter, GiveawayScheduler scheduler, GiveawayService service)
{
_db = db;
_formatter = formatter;
_scheduler = scheduler;
_service = service;
}

[DefaultMemberPermissions(GuildPermission.ManageEvents)]
[SlashCommand("start", "Starts a new giveaway in the current channel.")]
public async Task Start([ComplexParameter] GiveawayStartArgs args)
{
await DeferAsync();
var response = await GetOriginalResponseAsync();

var giveaway = new Giveaway()
{
MessageId = response.Id,
ChannelId = Context.Channel.Id,
GuildId = Context.Guild.Id,
Prize = args.Prize,
MaxWinners = args.MaxWinners,
ExpiresAt = args.ExpiresAt,
};

await _db.AddAsync(giveaway);
await _db.SaveChangesAsync();

_scheduler.Schedule(giveaway.MessageId, giveaway.ExpiresAt);

var props = _formatter.GetActiveMessageProperties(giveaway);
await FollowupAsync(embed: props.Embed.Value, components: props.Components.Value);
}

[ComponentInteraction("join:*")]
public async Task<RuntimeResult> Join(ulong messageId)
{
await DeferAsync(true);

var giveaway = await _db.Giveaways
.Include(g => g.Participants)
.FirstOrDefaultAsync(g => g.MessageId == messageId && g.Status == GiveawayStatus.Active);

if (giveaway == null)
return InteractionResult.FromError("There's no active giveaway associated with this message.");

if (await _service.GetGiveawayMessage(giveaway) is not IUserMessage message)
return InteractionResult.FromError("Unknown message!");

await _service.AddOrRemoveParticipantAsync(giveaway, Context.User.Id);

var modifiedProps = _formatter.GetActiveMessageProperties(giveaway);
await message.ModifyAsync(props =>
{
props.Embed = modifiedProps.Embed;
props.Components = modifiedProps.Components;
});

return InteractionResult.FromSuccess();
}

[ComponentInteraction("info:*")]
public async Task<RuntimeResult> Info(ulong messageId)
{
await DeferAsync(true);

var giveaway = await _db.Giveaways
.Include(g => g.Participants)
.FirstOrDefaultAsync(g => g.MessageId == messageId && g.Status != GiveawayStatus.Ended);

if (giveaway == null)
return InteractionResult.FromError("There's no active giveaway associated with this message.");

var props = _formatter.GetInfoMessageProperties(giveaway);
await FollowupAsync(embed: props.Embed.Value, ephemeral: true);

return InteractionResult.FromSuccess();
}

[DefaultMemberPermissions(GuildPermission.ManageEvents)]
[MessageCommand("End Giveaway")]
public async Task<RuntimeResult> End(IMessage message)
{
await DeferAsync(true);

if (await _db.Giveaways.AnyAsync(g => g.MessageId == message.Id && g.Status == GiveawayStatus.Ended))
return InteractionResult.FromError("There's no active giveaway associated with this message.");

await _service.ExpireAsync(message.Id);
_scheduler.ChangeState(message.Id, new SucceededState(0, 0, 0));

return InteractionResult.FromSuccess("This giveaway has just been ended! Winners have been notified via DMs.");
}
}
7 changes: 7 additions & 0 deletions src/Giveaways/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using Giveaways;
using Giveaways.Data;
using Giveaways.Services;
using Hangfire;
using Hangfire.Storage.SQLite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -18,6 +20,8 @@

builder.Services.AddNamedOptions<StartupOptions>();

builder.Services.AddHangfire(options => options.UseSQLiteStorage());
builder.Services.AddHangfireServer();
builder.Services.AddSqlite<AppDbContext>(builder.Configuration.GetConnectionString("Default"));

builder.Services.AddDiscordHost((config, _) =>
Expand Down Expand Up @@ -49,6 +53,9 @@

builder.Services.AddSingleton<InteractionRouter>();
builder.Services.AddHostedService<InteractionHandler>();

builder.Services.AddScoped<GiveawayFormatter>();
builder.Services.AddScoped<GiveawayService>();
builder.Services.AddSingleton<GiveawayScheduler>();

var host = builder.Build();
Expand Down
108 changes: 108 additions & 0 deletions src/Giveaways/Services/GiveawayFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Linq;
using Discord;
using Discord.WebSocket;
using Giveaways.Data;

namespace Giveaways.Services;

public class GiveawayFormatter
{
private readonly DiscordSocketClient _client;

public GiveawayFormatter(DiscordSocketClient client)
{
_client = client;
}

public MessageProperties GetActiveMessageProperties(Giveaway giveaway)
{
var relative = TimestampTag.FormatFromDateTime(giveaway.ExpiresAt, TimestampTagStyles.Relative);
var longDateTime = TimestampTag.FormatFromDateTime(giveaway.ExpiresAt, TimestampTagStyles.LongDateTime);

var embed = new EmbedBuilder()
.WithAuthor($"x{giveaway.MaxWinners} Giveaway Prizes", Icons.Gift)
.WithTitle(giveaway.Prize)
.AddField("Winners Selection Date", $"{relative} ({longDateTime})")
.WithFooter($"{giveaway.Participants.Count} participants")
.WithColor(Colors.Fuchsia)
.Build();

var components = new ComponentBuilder()
.WithButton("Join", $"join:{giveaway.MessageId}", ButtonStyle.Primary, new Emoji("🪅"))
.WithButton("Learn more", $"info:{giveaway.MessageId}", ButtonStyle.Secondary, new Emoji(""))
.Build();

return new MessageProperties
{
Embed = embed,
Components = components
};
}

public MessageProperties GetEndedMessageProperties(Giveaway giveaway)
{
var ids = giveaway.Winners.Select(w => MentionUtils.MentionUser(w.UserId));
var mentions = string.Join(", ", ids);

var embed = new EmbedBuilder()
.WithAuthor($"x{giveaway.MaxWinners} Giveaway Prizes", Icons.Confetti)
.WithTitle(giveaway.Prize)
.AddField("Winners", string.IsNullOrEmpty(mentions) ? "None" : mentions)
.WithFooter($"{giveaway.Participants.Count} participants")
.WithColor(Colors.Secondary)
.WithCurrentTimestamp()
.Build();

return new MessageProperties()
{
Embed = embed,
Components = null
};
}

public MessageProperties GetInfoMessageProperties(Giveaway giveaway)
{
var guild = _client.GetGuild(giveaway.GuildId);

var longDate = TimestampTag.FormatFromDateTime(giveaway.ExpiresAt, TimestampTagStyles.LongDate);
var longDateTime = TimestampTag.FormatFromDateTime(giveaway.ExpiresAt, TimestampTagStyles.LongDateTime);

var embed = new EmbedBuilder()
.WithTitle("About this event")
.AddField("How it works?", $"On {longDate} the app is going to choose **{giveaway.MaxWinners}** random winners.")
.AddField("Who can join?", $"{guild.EveryoneRole.Mention} can push the join button before {longDateTime} in order to enter.")
.WithFooter($"The event organizers of {guild.Name} are responsible for awarding prizes.")
.WithColor(Colors.Primary)
.Build();

return new MessageProperties
{
Embed = embed
};
}

public MessageProperties GetCongratsMessageProperties(GiveawayParticipant winner)
{
var guild = _client.GetGuild(winner.Giveaway.GuildId);

var embed = new EmbedBuilder()
.WithAuthor($"Congrats!", Icons.Confetti)
.WithTitle($"You won the {winner.Giveaway.Prize}!")
.WithDescription($"To get your prize, visit **{guild.Name}** and contact their event organizers.")
.WithFooter("The app is not responsible for awarding prizes.")
.WithColor(Colors.Fuchsia)
.Build();

var url = MessageUtils.FormatJumpUrl(winner.Giveaway.GuildId, winner.Giveaway.ChannelId, winner.Giveaway.MessageId);

var components = new ComponentBuilder()
.WithLink("Jump to Message", url, new Emoji("🎁"))
.Build();

return new MessageProperties
{
Embed = embed,
Components = components
};
}
}
Loading

0 comments on commit 2f4cc66

Please sign in to comment.