Skip to content
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
4 changes: 3 additions & 1 deletion src/Accord.Bot/BotServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static IServiceCollection AddDiscordBot(this IServiceCollection services,
.WithCommandGroup<GitHubChallengesCommandGroup>()
.WithCommandGroup<ChannelFlagCommandGroup>()
.WithCommandGroup<PermissionCommandGroup>()
.WithCommandGroup<CampaignCommandGroup>()
.WithCommandGroup<RunOptionCommandGroup>()
.WithCommandGroup<ReminderCommandGroup>()
.WithCommandGroup<ProfileCommandGroup>()
Expand All @@ -72,6 +73,7 @@ public static IServiceCollection AddDiscordBot(this IServiceCollection services,
.AddResponder<MemberUpdateResponder>()
.AddResponder<MessageCreateDeleteResponder>()
.AddResponder<ModerationActionResponder>()
.AddResponder<PromotionCampaignVoteResponder>()
.AddResponder<ReactionResponder>()
.AddResponder<ReadyResponder>()
.AddResponder<RoleUpdateResponder>()
Expand All @@ -85,4 +87,4 @@ public static IServiceCollection AddDiscordBot(this IServiceCollection services,

return services;
}
}
}
127 changes: 127 additions & 0 deletions src/Accord.Bot/CommandGroups/CampaignCommandGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Accord.Bot.Helpers;
using Accord.Services.PromotionCampaigns;
using MediatR;
using Remora.Commands.Attributes;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Conditions;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Results;

namespace Accord.Bot.CommandGroups;

[Group("campaign")]
public class CampaignCommandGroup(
ICommandContext commandContext,
IMediator mediator,
PermissionUserFactory permissionUserFactory,
FeedbackService feedbackService) : AccordCommandGroup
{
[Command("myself"), Description("Open a promotion campaign for yourself"), Ephemeral]
public async Task<IResult> CampaignMyself(IRole role, IGuildMember vouchedForByUser)
{
if (!vouchedForByUser.User.HasValue)
{
return await feedbackService.SendContextualAsync("Vouching user not found");
}

var executingUser = await commandContext.ToPermissionUser(permissionUserFactory);
var vouchingUser = await permissionUserFactory.FromId(vouchedForByUser.User.Value.ID.Value);

var response = await mediator.Send(new CreatePromotionCampaignRequest(
executingUser,
executingUser,
vouchingUser,
role.ID.Value));

await response.GetAction(
async () => await feedbackService.SendContextualAsync("Campaign started!"),
async () => await feedbackService.SendContextualAsync(response.ErrorMessage));

return Result.FromSuccess();
}

[Command("for"), Description("Open a promotion campaign for another user"), Ephemeral]
public async Task<IResult> CampaignFor(IGuildMember user, IRole role)
{
if (!user.User.HasValue)
{
return await feedbackService.SendContextualAsync("Campaign user not found");
}

var targetUser = await permissionUserFactory.FromId(user.User.Value.ID.Value);
var vouchingUser = await commandContext.ToPermissionUser(permissionUserFactory);

var response = await mediator.Send(new CreatePromotionCampaignRequest(
targetUser,
vouchingUser,
vouchingUser,
role.ID.Value));

await response.GetAction(
async () => await feedbackService.SendContextualAsync("Campaign started!"),
async () => await feedbackService.SendContextualAsync(response.ErrorMessage));

return Result.FromSuccess();
}

[RequireDiscordPermission(DiscordPermission.Administrator), Command("close"), Description("Close a promotion campaign without promoting the user"), Ephemeral]
public async Task<IResult> Close(int campaignId)
{
var user = commandContext.GetExecutingUser();
var response = await mediator.Send(new ClosePromotionCampaignRequest(campaignId, user.ID.Value));

await response.GetAction(
async () => await feedbackService.SendContextualAsync($"Campaign #{campaignId} closed!"),
async () => await feedbackService.SendContextualAsync(response.ErrorMessage));

return Result.FromSuccess();
}

[RequireDiscordPermission(DiscordPermission.Administrator), Command("approve"), Description("Approve a promotion campaign and apply the role"), Ephemeral]
public async Task<IResult> Approve(int campaignId)
{
var user = commandContext.GetExecutingUser();
var response = await mediator.Send(new ApprovePromotionCampaignRequest(campaignId, user.ID.Value));

await response.GetAction(
async () => await feedbackService.SendContextualAsync($"Campaign #{campaignId} approved"),
async () => await feedbackService.SendContextualAsync(response.ErrorMessage));

return Result.FromSuccess();
}

[RequireDiscordPermission(DiscordPermission.Administrator), Command("list"), Description("List active promotion campaigns"), Ephemeral]
public async Task<IResult> List()
{
var campaigns = await mediator.Send(new GetActivePromotionCampaignsRequest());

if (!campaigns.Any())
{
return await feedbackService.SendContextualEmbedAsync(new Embed(
Title: "Active Promotion Campaigns",
Description: "No active campaigns."));
}

var description = new StringBuilder();

foreach (var campaign in campaigns)
{
description.AppendLine($"**#{campaign.Id}** {DiscordFormatter.UserIdToMention(campaign.ForUserId)} for {DiscordFormatter.RoleIdToMention(campaign.ToDiscordRoleId)}");
description.AppendLine($"Started by {DiscordFormatter.UserIdToMention(campaign.ByUserId)} · vouched by {DiscordFormatter.UserIdToMention(campaign.VouchedForByUserId)} · closes {DiscordFormatter.TimeToMarkdown(campaign.EndDateTime, TimeToMentionType.RelativeTime)}");
description.AppendLine();
}

var embed = new Embed(
Title: "Active Promotion Campaigns",
Description: description.ToString());

return await feedbackService.SendContextualEmbedAsync(embed);
}
}
26 changes: 6 additions & 20 deletions src/Accord.Bot/CommandGroups/ChannelFlagCommandGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,34 @@ public class ChannelFlagCommandGroup(ICommandContext commandContext,
FeedbackService feedbackService) : AccordCommandGroup
{
[Command("add"), Description("Add flag to the current channel")]
public async Task<IResult> AddFlag(string type, IChannel? channel = null)
public async Task<IResult> AddFlag(ChannelFlagType type, IChannel? channel = null)
{
var isParsedEnumValue = Enum.TryParse<ChannelFlagType>(type, out var actualChannelFlag);

if (!isParsedEnumValue || !Enum.IsDefined(actualChannelFlag))
{
return await feedbackService.SendContextualAsync("Type of flag is not found");
}

var proxy = commandContext.GetCommandProxy();
var user = await commandContext.ToPermissionUser(permissionUserFactory);

var channelId = channel?.ID.Value ?? proxy.ChannelId.Value;

var response = await mediator.Send(new AddChannelFlagRequest(user, actualChannelFlag, channelId));
var response = await mediator.Send(new AddChannelFlagRequest(user, type, channelId));

await response.GetAction(
async () => await feedbackService.SendContextualAsync($"{actualChannelFlag} flag added"),
async () => await feedbackService.SendContextualAsync($"{type} flag added"),
async () => await feedbackService.SendContextualAsync(response.ErrorMessage));

return Result.FromSuccess();
}

[Command("remove"), Description("Add flag to the current channel")]
public async Task<IResult> RemoveFlag(string type, IChannel? channel = null)
public async Task<IResult> RemoveFlag(ChannelFlagType type, IChannel? channel = null)
{
var isParsedEnumValue = Enum.TryParse<ChannelFlagType>(type, out var actualChannelFlag);

if (!isParsedEnumValue || !Enum.IsDefined(actualChannelFlag))
{
return await feedbackService.SendContextualAsync("Type of flag is not found");
}

var user = await commandContext.ToPermissionUser(permissionUserFactory);

var proxy = commandContext.GetCommandProxy();
var channelId = channel?.ID.Value ?? proxy.ChannelId.Value;

var response = await mediator.Send(new DeleteChannelFlagRequest(user, actualChannelFlag, channelId));
var response = await mediator.Send(new DeleteChannelFlagRequest(user, type, channelId));

await response.GetAction(
async () => await feedbackService.SendContextualAsync($"{actualChannelFlag} flag removed"),
async () => await feedbackService.SendContextualAsync($"{type} flag removed"),
async () => await feedbackService.SendContextualAsync(response.ErrorMessage));

return Result.FromSuccess();
Expand Down
34 changes: 9 additions & 25 deletions src/Accord.Bot/CommandGroups/PermissionCommandGroup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.ComponentModel;
using System.ComponentModel;
using System.Threading.Tasks;
using Accord.Bot.Helpers;
using Accord.Domain.Model;
Expand All @@ -17,32 +16,17 @@ namespace Accord.Bot.CommandGroups;
[Group("permission")]
public class PermissionCommandGroup(IMediator mediator, FeedbackService feedbackService) : AccordCommandGroup
{
[RequireDiscordPermission(DiscordPermission.Administrator), Command("adduser"), Description("Add permission to a user"), Ephemeral]
public async Task<IResult> AddPermissionToMember(IGuildMember member, string type)
[RequireDiscordPermission(DiscordPermission.Administrator), Command("add-user"), Description("Add permission to a user"), Ephemeral]
public async Task<IResult> AddPermissionToMember(IGuildMember member, PermissionType type)
{
if (!Enum.TryParse<PermissionType>(type, out var actualPermission) || !Enum.IsDefined(actualPermission))
{
return await feedbackService.SendContextualAsync("Permission is not found");
}

if (!member.User.HasValue)
{
return Result.FromSuccess();
}

await mediator.Send(new AddPermissionForUserRequest(member.User.Value.ID.Value, actualPermission));
return await feedbackService.SendContextualAsync($"{actualPermission} permission added to {member.User.Value.ID.ToUserMention()}");
await mediator.Send(new AddPermissionForUserRequest(member.User.Value.ID.Value, type));
return await feedbackService.SendContextualAsync($"{type} permission added to {member.User.Value.ID.ToUserMention()}");
}

[RequireDiscordPermission(DiscordPermission.Administrator), Command("addrole"), Description("Add permission to a role"), Ephemeral]
public async Task<IResult> AddPermissionToRole(IRole role, string type)
[RequireDiscordPermission(DiscordPermission.Administrator), Command("add-role"), Description("Add permission to a role"), Ephemeral]
public async Task<IResult> AddPermissionToRole(IRole role, PermissionType type)
{
if (!Enum.TryParse<PermissionType>(type, out var actualPermission) || !Enum.IsDefined(actualPermission))
{
return await feedbackService.SendContextualAsync("Permission is not found");
}

await mediator.Send(new AddPermissionForRoleRequest(role.ID.Value, actualPermission));
return await feedbackService.SendContextualAsync($"{actualPermission} permission added to `{role.Name}`");
await mediator.Send(new AddPermissionForRoleRequest(role.ID.Value, type));
return await feedbackService.SendContextualAsync($"{type} permission added to `{role.Name}`");
}
}
11 changes: 3 additions & 8 deletions src/Accord.Bot/CommandGroups/RunOptionCommandGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,13 @@ namespace Accord.Bot.CommandGroups;
public class RunOptionCommandGroup(IMediator mediator, FeedbackService feedbackService) : AccordCommandGroup
{
[RequireDiscordPermission(DiscordPermission.Administrator), Command("configure"), Description("Configure an option for the bot"), Ephemeral]
public async Task<IResult> Configure(string type, string value)
public async Task<IResult> Configure(RunOptionKey type, string value)
{
if (!Enum.TryParse<RunOptionKey>(type, out var actualRunOptionType) || !Enum.IsDefined(actualRunOptionType))
{
return await feedbackService.SendContextualAsync("Configuration is not found");
}

var response = await mediator.Send(new UpdateRunOptionRequest(actualRunOptionType, value));
var response = await mediator.Send(new UpdateRunOptionRequest(type, value));

if (response.Success)
{
return await feedbackService.SendContextualAsync($"{actualRunOptionType} configuration updated to {value}");
return await feedbackService.SendContextualAsync($"{type} configuration updated to {value}");
}

return await feedbackService.SendContextualAsync(response.ErrorMessage);
Expand Down
69 changes: 69 additions & 0 deletions src/Accord.Bot/Helpers/PromotionCampaignMessageFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Drawing;
using Accord.Services.PromotionCampaigns;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;

namespace Accord.Bot.Helpers;

[RegisterScoped]
public class PromotionCampaignMessageFactory(ThumbnailHelper thumbnailHelper)
{
public const string VOTE_CUSTOM_ID_PREFIX = "promotion-campaign-vote:";

public Embed CreateEmbed(IUser user, PromotionCampaignDto campaign)
{
var avatar = thumbnailHelper.GetAvatar(user);
var formattedTime = DiscordFormatter.TimeToMarkdown(campaign.EndDateTime);
var userMention = DiscordFormatter.UserIdToMention(campaign.ForUserId);
var roleMention = DiscordFormatter.RoleIdToMention(campaign.ToDiscordRoleId);

var title = $"{user.Username} is campaigning";
var description = $"{userMention} is campaigning for {roleMention} until {formattedTime}.";

var colour = campaign.ClosedDateTime is not null
? campaign.IsApproved
? Color.Green
: Color.DimGray
: Color.DodgerBlue;

if (campaign.ClosedDateTime is not null)
{
title = $"{user.Username} campaigned";
description = $"{userMention} campaigned for {roleMention}";

if (campaign.IsApproved)
{
var closedFormattedTime = DiscordFormatter.TimeToMarkdown(campaign.ClosedDateTime.Value);
description += $" and was successful on {closedFormattedTime} 🎉";
colour = Color.Green;
}
}

return new Embed(
Title: title,
Thumbnail: avatar,
Description: description,
Colour: colour,
Fields: new[]
{
new EmbedField("Required votes", campaign.VoteThresholdRequired.ToString(), true),
new EmbedField("Obtained votes", $"{campaign.VoteProgress}", true),
},
Footer: new EmbedFooter($"#{campaign.Id} - Achieving required votes does not guarantee promotion!"));
}

public IReadOnlyCollection<IMessageComponent> CreateComponents(int campaignId)
{
return
[
new ActionRowComponent([
new ButtonComponent(
ButtonComponentStyle.Success,
"+1",
default,
$"{VOTE_CUSTOM_ID_PREFIX}{campaignId}")
])
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Threading;
using System.Threading.Tasks;
using Accord.Services;
using Accord.Services.PromotionCampaigns;
using MediatR;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Rest.Core;

namespace Accord.Bot.RequestHandlers;

public class ApplyPromotionCampaignRoleToDiscordHandler(
IDiscordRestGuildAPI guildApi,
DiscordConfiguration discordConfiguration) : IRequestHandler<ApplyPromotionCampaignRoleToDiscordRequest, bool>
{
public async Task<bool> Handle(
ApplyPromotionCampaignRoleToDiscordRequest request,
CancellationToken cancellationToken)
{
var response = await guildApi.AddGuildMemberRoleAsync(
new Snowflake(discordConfiguration.GuildId),
new Snowflake(request.DiscordUserId),
new Snowflake(request.DiscordRoleId),
$"Campaign #{request.PromotionCampaignId} approved by {request.ApprovedByUserId}",
cancellationToken);

return response.IsSuccess;
}
}
Loading
Loading