Skip to content

Implement Achievement/Badge System with Admin Commands - #344

Merged
Pierre-Demessence merged 25 commits into
masterfrom
copilot/fix-341
Sep 13, 2026
Merged

Pierre-Demessence merged 25 commits into
masterfrom
copilot/fix-341

Conversation

Copilot AI commented Jul 21, 2025 •

Copy link
Copy Markdown

This PR implements a comprehensive badge/achievement system for the UDC Bot, allowing administrators to create, assign, and manage user badges through Discord slash commands.

Features Implemented

Database Layer

  • Badge Entity: Stores badge information (ID, Title, Description, CreatedAt)
  • UserBadge Entity: Junction table linking users to badges with award metadata
  • Auto-table Creation: Database tables are automatically created on bot startup
  • MySQL Integration: Uses existing Insight.Database ORM patterns

Admin Slash Commands

All commands require Discord Administrator permissions and use the /badge command group:

  • /badge create [title] [description] - Create new badges with validation
  • /badge assign [user] [badge] - Assign badges to users with duplicate prevention
  • /badge remove [user] [badge] - Remove badges from users
  • /badge list - Display all available badges
  • /badge view [user] - Show a user's earned badges

User Experience

  • Rich embed responses with appropriate colors and formatting
  • DM notifications when users receive badges
  • Comprehensive error handling and validation
  • Clear feedback for all operations

Database Schema

The system creates two new tables:

-- Stores badge definitions
CREATE TABLE `badges` (
    `Id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `Title` varchar(100) NOT NULL,
    `Description` text NOT NULL,
    `CreatedAt` datetime NOT NULL,
    PRIMARY KEY (`Id`),
    UNIQUE KEY `Title` (`Title`)
);

-- Links users to their badges
CREATE TABLE `user_badges` (
    `Id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `UserID` varchar(32) NOT NULL,
    `BadgeId` int(11) UNSIGNED NOT NULL,
    `AwardedAt` datetime NOT NULL,
    `AwardedBy` varchar(32) NOT NULL,
    PRIMARY KEY (`Id`),
    UNIQUE KEY `user_badge_unique` (`UserID`, `BadgeId`),
    FOREIGN KEY (`BadgeId`) REFERENCES `badges` (`Id`) ON DELETE CASCADE
);

Usage Examples

/badge create title:"UDC Jam Winner" description:"Won a UDC Game Jam competition"
/badge assign user:@JohnDoe badge:"UDC Jam Winner"
/badge view user:@JohnDoe
/badge list
/badge remove user:@JohnDoe badge:"UDC Jam Winner"

Technical Details

  • Follows existing codebase patterns and conventions
  • Integrates with current dependency injection system
  • Uses established logging and error handling patterns
  • Maintains consistency with Discord.NET interaction patterns
  • Includes comprehensive input validation and security measures

The system is production-ready and will be immediately available to administrators once deployed.

Fixes #341.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: Pierre-Demessence <1756398+Pierre-Demessence@users.noreply.github.com>
Copilot AI changed the title [WIP] Achievement/Badge System Implement Achievement/Badge System with Admin Commands Jul 21, 2025
Copilot AI requested a review from Pierre-Demessence July 21, 2025 13:48
@Pierre-Demessence

Copy link
Copy Markdown
Member

/deploy_dev

@github-actions

Copy link
Copy Markdown

🚀 Starting deployment of Unity-Developer-Community/UDC-Bot:copilot/fix-341 to development...

@Pierre-Demessence

Copy link
Copy Markdown
Member

@copilot A few modifications:

  • Users should be able to view the list of badges

  • Anyone should be able to view the list of badges of another user

    • (Still only Admins can Create/Assign badges).
  • Admins should be able to edit a Badge too.

  • A new property: Whether badges should be public or not (default to public).

    • Private badges are only visible to Admins (on the full list + on the list of badges assigned to an user). Other users won't see them.

Co-authored-by: Pierre-Demessence <1756398+Pierre-Demessence@users.noreply.github.com>

Copilot AI commented Jul 26, 2025

Copy link
Copy Markdown
Author

@copilot A few modifications:

  • Users should be able to view the list of badges
  • Anyone should be able to view the list of badges of another user
    ...

I've implemented all the requested modifications to the badge system:

✅ Users can now view the list of badges - Removed admin requirement from /badge list
✅ Anyone can view badges of other users - Removed admin requirement from /badge view
✅ Admins can edit badges - Added new /badge edit command for administrators
✅ Public/private badge visibility - Added IsPublic property (defaults to public)
✅ Private badge filtering - Private badges only visible to admins with 🔒 indicator

Key Changes:

  • Badge visibility system with automatic database migration
  • Permission restructuring: list/view commands are now public, admin commands require permissions
  • Private badges show lock icon (🔒) for admins and are filtered from non-admin views
  • Enhanced embed responses showing visibility status and counts
  • Public responses for engagement vs ephemeral for admin operations

The system is backward compatible and will automatically add the IsPublic column to existing installations. Commit: e2061f0

@Pierre-Demessence

Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request

Copilot AI and others added 3 commits September 10, 2026 14:57
# Conflicts:
#	DiscordBot/Services/DatabaseService.cs

Co-authored-by: Pierre-Demessence <1756398+Pierre-Demessence@users.noreply.github.com>
Co-authored-by: Pierre-Demessence <1756398+Pierre-Demessence@users.noreply.github.com>
Co-authored-by: Pierre-Demessence <1756398+Pierre-Demessence@users.noreply.github.com>

Copilot AI commented Sep 10, 2026

Copy link
Copy Markdown
Author

@copilot resolve the merge conflicts in this pull request

Resolved. I merged master into this branch and fixed the resulting conflicts in ff9ae56, then applied the follow-up review fixes in 10792f7.

{
try
{
var c = new NpgsqlConnection(ConnectionString);
Comment thread DiscordBot/Modules/BadgeSlashModule.cs Fixed
Comment thread DiscordBot/Modules/BadgeSlashModule.cs Fixed
/// </summary>
public bool IsUserAdmin(SocketGuildUser user)
{
return user?.GuildPermissions.Administrator == true;
Comment thread DiscordBot/Modules/BadgeSlashModule.cs Fixed
Comment on lines +54 to +59
catch (Exception e)
{
await _logging.Log(LogBehaviour.ConsoleChannelAndFile,
$"Error creating badge '{title}': {e}", ExtendedLogSeverity.Error);
return null;
}
Comment on lines +104 to +109
catch (Exception e)
{
await _logging.Log(LogBehaviour.ConsoleChannelAndFile,
$"Error updating badge ID {badgeId}: {e}", ExtendedLogSeverity.Error);
return null;
}
Comment on lines +130 to +135
catch (Exception e)
{
await _logging.Log(LogBehaviour.ConsoleChannelAndFile,
$"Error retrieving badges: {e}", ExtendedLogSeverity.Error);
return new List<Badge>();
}
Comment on lines +148 to +153
catch (Exception e)
{
await _logging.Log(LogBehaviour.ConsoleChannelAndFile,
$"Error retrieving badge {badgeId}: {e}", ExtendedLogSeverity.Error);
return null;
}
Comment on lines +166 to +171
catch (Exception e)
{
await _logging.Log(LogBehaviour.ConsoleChannelAndFile,
$"Error retrieving badge '{title}': {e}", ExtendedLogSeverity.Error);
return null;
}
@Pierre-Demessence

Copy link
Copy Markdown
Member

@copilot I want a way to add a "leaderboard" for some badges. For example I'll add one badge for each UDC Jam we did, and award it to the past winners of those jams. So "UDC Jam #1 Winner", "UDC Jam #2 Winner", etc... And it would be nice to have a leaderboard of badges. A global leaderboard is nice (who has the most badge overall), but also a leaderboard only for "UDC Jam" badges. So we need a way to group/tag badges, so we can do something like /badge leaderboard group:udcjam for eample.
Before actually working on the code, propose a solution/design in response in comment in the PR, and ait for my validation before actually working on it.

Copilot AI commented Sep 10, 2026

Copy link
Copy Markdown
Author

@copilot I want a way to add a "leaderboard" for some badges. For example I'll add one badge for each UDC Jam we did, and award it to the past winners of those jams. So "UDC Jam #1 Winner", "UDC Jam #...

Proposed approach:

  1. Add an optional group/tag field on badges (for example GroupKey and optionally a display label). For your case, all UDC Jam #N Winner badges would share the same group key like udcjam.
  2. Keep it 1 badge -> 0/1 group for now. That covers the leaderboard use case with the smallest schema/API change. If later needed, we can evolve to many-to-many tags.
  3. Add leaderboard queries at 2 levels:
    • global: count all visible badges per user
    • by group: count only visible badges whose GroupKey matches the requested group
  4. Respect existing visibility rules:
    • non-admins only count public badges
    • admins can see/count private badges too
  5. Slash command shape:
    • /badge leaderboard -> global leaderboard
    • /badge leaderboard group:udcjam -> filtered leaderboard
    • optionally later: /badge groups to list available groups
  6. Admin management:
    • extend /badge create and /badge edit with an optional group parameter
    • no extra assignment change needed, since grouping belongs to the badge definition
  7. DB/API shape:
    • add nullable GroupKey column on badges
    • migration for existing installs adds the column
    • add repo methods for grouped badge queries and leaderboard aggregation

If you validate this direction, I’ll implement it with the minimal schema/service/command changes.

udc-bot-deploy Bot pushed a commit that referenced this pull request Sep 13, 2026
@github-actions

Copy link
Copy Markdown

✅ PR deployed to dev!

Image: ghcr.io/unity-developer-community/udc-bot-dev:660c2b0
ArgoCD will sync shortly.

@github-actions
github-actions Bot temporarily deployed to dev September 13, 2026 14:51 Inactive
… and navigation components for badge listing

Co-authored-by: Copilot <copilot@github.com>
Comment on lines +268 to +274
catch (Exception e)
{
await _logging.Log(LogBehaviour.ConsoleChannelAndFile,
$"Error removing badge '{badge.Title}' from user {user.GetPreferredAndUsername()}: {e}",
ExtendedLogSeverity.Error);
return false;
}
udc-bot-deploy Bot pushed a commit that referenced this pull request Sep 13, 2026
@github-actions

Copy link
Copy Markdown

✅ PR deployed to dev!

Image: ghcr.io/unity-developer-community/udc-bot-dev:3bea3d1
ArgoCD will sync shortly.

@github-actions
github-actions Bot temporarily deployed to dev September 13, 2026 16:23 Inactive
udc-bot-deploy Bot pushed a commit that referenced this pull request Sep 13, 2026
@github-actions

Copy link
Copy Markdown

✅ PR deployed to dev!

Image: ghcr.io/unity-developer-community/udc-bot-dev:3601b21
ArgoCD will sync shortly.

@github-actions
github-actions Bot temporarily deployed to dev September 13, 2026 16:47 Inactive
Pierre-Demessence and others added 2 commits September 13, 2026 18:53
Allow admin badge commands to resolve badges by ID or title with autocomplete hints.

Make /admin badge edit fields optional and no-op safely when no changes are provided.

Add badge deletion service support used by admin delete flow.
…prove user feedback

Co-authored-by: Copilot <copilot@github.com>
Comment on lines +149 to +154
catch (Exception e)
{
await _logging.Log(LogBehaviour.ConsoleChannelAndFile,
$"Error deleting badge ID {badgeId}: {e}", ExtendedLogSeverity.Error);
return null;
}
udc-bot-deploy Bot pushed a commit that referenced this pull request Sep 13, 2026
@github-actions

Copy link
Copy Markdown

✅ PR deployed to dev!

Image: ghcr.io/unity-developer-community/udc-bot-dev:70844be
ArgoCD will sync shortly.

@github-actions
github-actions Bot temporarily deployed to dev September 13, 2026 16:59 Inactive
@Pierre-Demessence
Pierre-Demessence marked this pull request as ready for review September 13, 2026 17:04
Copilot AI lite review requested due to automatic review settings September 13, 2026 17:04
@Pierre-Demessence
Pierre-Demessence added this pull request to the merge queue Sep 13, 2026
Merged via the queue into master with commit 520e7bd Sep 13, 2026
7 checks passed
@Pierre-Demessence
Pierre-Demessence deleted the copilot/fix-341 branch September 13, 2026 17:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Nullable-contract mismatches (non-nullable returns that return null) and documented UX/DB discrepancies need resolving to avoid build warnings/runtime confusion.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds a new badge/achievement subsystem to UDC-Bot, including PostgreSQL-backed persistence and new Discord slash-command surfaces for users and admins.

Changes:

  • Introduces badge persistence (entities + Insight.Database repository) and startup-time table initialization/migrations.
  • Adds BadgeService plus new slash commands for viewing/listing/leaderboards (/badge) and admin management (/admin badge ...).
  • Includes several small cleanup/typo fixes in existing modules/services.
File summaries
File Description
DiscordBot/Services/Profiles/BirthdayAnnouncementService.cs Minor logging/initialization tweaks in birthday announcements.
DiscordBot/Services/DatabaseService.cs Adds badge repo accessor and badge table initialization/creation on startup.
DiscordBot/Services/BadgeService.cs New service layer for creating/updating/deleting/assigning/querying badges.
DiscordBot/Program.cs Registers BadgeService in DI.
DiscordBot/Modules/Utils/ConvertModule.cs Fixes command summary text and avoids redundant ToLower calls.
DiscordBot/Modules/Server/ServerSlashModule.cs Corrects help title/module key usage for server commands.
DiscordBot/Modules/Profiles/RankModule.cs Fixes typo in summary text.
DiscordBot/Modules/Profiles/ProfileModule.cs Removes unused variable.
DiscordBot/Modules/Fun/FunModule.cs Adds clarifying comment for legacy behavior.
DiscordBot/Modules/BadgeSlashModule.cs New public /badge commands (list/view/leaderboard + paging components).
DiscordBot/Modules/AdminSlashModule.cs Adds /admin badge admin commands + small adjustments in existing admin flows.
DiscordBot/Extensions/BadgeRepository.cs New badge/user_badge models, props, and SQL repo interface for PostgreSQL.
DiscordBot.Tests/Extensions/BadgeRepositoryTests.cs Adds unit test coverage for EnsureBadgeDetails hydration behavior.
Review details

Suppressed comments (3)

DiscordBot/Services/BadgeService.cs:80

  • UpdateBadge() returns null when the repo is unavailable, the badge is missing, a title conflicts, or on exceptions, but the return type is Task (non-nullable). Update the signature to Task<Badge?> to match the actual contract under nullable reference types.
    public async Task<Badge> UpdateBadge(int badgeId, string title, string description, bool isPublic, string? groupKey = null, bool updateGroup = false)
    {
        try
        {
            var badgeQuery = _databaseService.BadgeQuery;
            if (badgeQuery == null)
                return null;

DiscordBot/Services/BadgeService.cs:192

  • GetBadge() can return null (e.g., when BadgeQuery is null or an exception occurs), but it is declared as Task. With nullable enabled, the signature should be Task<Badge?> so callers are forced to handle the not-found/unavailable cases safely.
    public async Task<Badge> GetBadge(int badgeId)
    {
        try
        {
            var badgeQuery = _databaseService.BadgeQuery;
            return badgeQuery == null ? null : await badgeQuery.GetBadge(badgeId);
        }

DiscordBot/Extensions/BadgeRepository.cs:55

  • BadgeLeaderboardEntry.UserId is non-nullable but not initialized, which triggers CS8618 with nullable enabled. Give it a default value (or make it nullable if needed).
    public string UserId { get; set; }
    public long BadgeCount { get; set; }
  • Files reviewed: 13/13 changed files
  • Comments generated: 9
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +8 to +9
public string Title { get; set; }
public string Description { get; set; }
Comment on lines +103 to +107
[Sql($"SELECT * FROM {BadgeProps.TableName} WHERE {BadgeProps.Id} = @badgeId")]
Task<Badge> GetBadge(int badgeId);

[Sql($"SELECT * FROM {BadgeProps.TableName} WHERE {BadgeProps.Title} = @title")]
Task<Badge> GetBadgeByTitle(string title);
/// <summary>
/// Creates a new badge with the specified title and description.
/// </summary>
public async Task<Badge> CreateBadge(string title, string description, bool isPublic = true, string? groupKey = null)
Comment on lines +355 to +358
catch
{
await _logging.LogAction($"DatabaseService: Badge tables do not exist, attempting to generate tables.",
ExtendedLogSeverity.LowWarning);
Comment on lines 12 to +18
[Group("admin", "Administrator-only commands")]
[DefaultMemberPermissions(GuildPermission.Administrator)]
public class AdminSlashModule : InteractionModuleBase
{
[Group("badge", "Badge administration commands")]
public class BadgeAdminCommands : InteractionModuleBase<SocketInteractionContext>
{
Comment on lines +208 to +223
var success = await BadgeService.AssignBadgeToUser(user, badge, Context.User as SocketGuildUser);

if (success)
{
var embed = new EmbedBuilder()
.WithTitle("🏆 Badge Assigned Successfully")
.WithDescription($"Assigned **{badge.Title}** to **{user.DisplayName}**")
.AddField("Badge Description", badge.Description)
.AddField("Assigned By", Context.User.Mention)
.AddField("Assigned At", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC"))
.WithColor(Color.Gold)
.WithTimestamp(DateTimeOffset.UtcNow)
.Build();

await Context.Interaction.FollowupAsync(embed: embed, ephemeral: true);
}
Comment on lines 861 to 867
}
else
{
var guildUser = await Context.Guild.GetUserAsync(userId);
var guildUser = Context.Guild.GetUser(userId);
displayName = guildUser?.DisplayName ?? $"Unknown User ({entry.UserID})";
}

Comment on lines +18 to +20
public BadgeService BadgeService { get; set; }
public ILoggingService LoggingService { get; set; }

Comment on lines +49 to +55
private IBadgeRepo? CreateBadgeQuery()
{
try
{
var c = new NpgsqlConnection(ConnectionString);
return c.As<IBadgeRepo>();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Achievement/Badge System

3 participants