Preconditions are attributes that determine whether a command can be executed based on specific criteria. They run before the command executes and can block execution if requirements aren't met.
Preconditions allow you to:
Fluxer.Net provides several built-in preconditions:
Restricts the command to the bot owner only:
using Fluxer.Net.Commands.Attributes;
[Command("shutdown")]
[RequireOwner]
public async Task ShutdownCommand()
{
await ReplyAsync("Shutting down...");
Environment.Exit(0);
}
Note: Set the BOT_OWNER_ID environment variable to your user ID.
Restricts where the command can be used:
[Command("serverinfo")]
[RequireContext(ContextType.Guild)] // Guild only
public async Task ServerInfoCommand()
{
await ReplyAsync($"Guild: {Context.GuildId}");
}
[Command("private")]
[RequireContext(ContextType.DM)] // DM only
public async Task PrivateCommand()
{
await ReplyAsync("This is a private command!");
}
// Multiple contexts (Guild OR DM)
[RequireContext(ContextType.Guild | ContextType.DM)]
public async Task FlexibleCommand()
{
// Works in both guilds and DMs
}
Requires the user to have specific permissions:
using Fluxer.Net.Data.Enums;
[Command("ban")]
[RequireUserPermission(Permissions.BanMembers)]
public async Task BanCommand(ulong userId)
{
// Only users with Ban Members permission can use this
await ReplyAsync($"Banned user {userId}");
}
[Command("manage")]
[RequireUserPermission(Permissions.ManageGuild | Permissions.Administrator)]
public async Task ManageCommand()
{
// Requires either Manage Guild OR Administrator
}
Ensures the bot has necessary permissions:
[Command("kick")]
[RequireBotPermission(Permissions.KickMembers)]
public async Task KickCommand(ulong userId)
{
// Bot must have Kick Members permission
await Context.Client.KickGuildMember(Context.GuildId.Value, userId);
await ReplyAsync($"Kicked user {userId}");
}
To create a custom precondition, inherit from PreconditionAttribute and implement CheckPermissionsAsync:
using Fluxer.Net.Commands;
using Fluxer.Net.Commands.Attributes;
public class MyCustomPreconditionAttribute : PreconditionAttribute
{
public override Task CheckPermissionsAsync(
CommandContext context,
CommandInfo command,
IServiceProvider? services)
{
// Check your conditions
bool conditionMet = /* your logic here */;
if (conditionMet)
return Task.FromResult(PreconditionResult.FromSuccess());
return Task.FromResult(PreconditionResult.FromError("Condition not met!"));
}
}
Prevent users from spamming commands:
using System.Collections.Concurrent;
using Fluxer.Net.Commands;
using Fluxer.Net.Commands.Attributes;
public class CooldownAttribute : PreconditionAttribute
{
private static readonly ConcurrentDictionary<(ulong UserId, string Command), DateTime> _cooldowns = new();
private readonly int _seconds;
public CooldownAttribute(int seconds)
{
_seconds = seconds;
}
public override Task CheckPermissionsAsync(
CommandContext context,
CommandInfo command,
IServiceProvider? services)
{
var key = (context.User.Id, command.Name);
var now = DateTime.UtcNow;
if (_cooldowns.TryGetValue(key, out var lastUsed))
{
var timeLeft = (lastUsed.AddSeconds(_seconds) - now).TotalSeconds;
if (timeLeft > 0)
{
return Task.FromResult(PreconditionResult.FromError(
$"Please wait {timeLeft:F1} seconds before using this command again."));
}
}
_cooldowns[key] = now;
// Clean up old entries (optional)
CleanupOldEntries(now);
return Task.FromResult(PreconditionResult.FromSuccess());
}
private void CleanupOldEntries(DateTime now)
{
var expiredKeys = _cooldowns
.Where(kvp => (now - kvp.Value).TotalSeconds > _seconds)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expiredKeys)
{
_cooldowns.TryRemove(key, out _);
}
}
}
// Usage:
[Command("daily")]
[Cooldown(86400)] // 24 hours
public async Task DailyCommand()
{
await ReplyAsync("You claimed your daily reward!");
}
Require users to have a specific role:
public class RequireRoleAttribute : PreconditionAttribute
{
private readonly ulong _roleId;
public RequireRoleAttribute(ulong roleId)
{
_roleId = roleId;
}
public override async Task CheckPermissionsAsync(
CommandContext context,
CommandInfo command,
IServiceProvider? services)
{
// Must be in a guild
if (!context.GuildId.HasValue)
return PreconditionResult.FromError("This command must be used in a guild.");
try
{
// Fetch guild member to get their roles
var member = await context.Client.GetGuildMember(context.GuildId.Value, context.User.Id);
if (member.Roles.Contains(_roleId))
return PreconditionResult.FromSuccess();
return PreconditionResult.FromError("You don't have the required role to use this command.");
}
catch (Exception ex)
{
return PreconditionResult.FromError($"Error checking roles: {ex.Message}");
}
}
}
// Usage:
[Command("vip")]
[RequireRole(123456789012345678)] // VIP role ID
public async Task VipCommand()
{
await ReplyAsync("Welcome, VIP member!");
}
Restrict commands to premium users using a database or external service:
public class RequirePremiumAttribute : PreconditionAttribute
{
public override async Task CheckPermissionsAsync(
CommandContext context,
CommandInfo command,
IServiceProvider? services)
{
// Get database service from dependency injection
var database = services?.GetService(typeof(IDatabaseService)) as IDatabaseService;
if (database == null)
return PreconditionResult.FromError("Database service not available.");
try
{
bool isPremium = await database.IsUserPremiumAsync(context.User.Id);
if (isPremium)
return PreconditionResult.FromSuccess();
return PreconditionResult.FromError(
"This command is only available to premium users. Use `/premium` to subscribe!");
}
catch (Exception ex)
{
return PreconditionResult.FromError($"Error checking premium status: {ex.Message}");
}
}
}
// Usage:
[Command("exclusive")]
[RequirePremium]
public async Task ExclusiveCommand()
{
await ReplyAsync("Thanks for being a premium member!");
}
Require commands to be used in NSFW-marked channels:
public class RequireNsfwAttribute : PreconditionAttribute
{
public override async Task CheckPermissionsAsync(
CommandContext context,
CommandInfo command,
IServiceProvider? services)
{
try
{
var channel = await context.Client.GetChannel(context.ChannelId);
if (channel.Nsfw == true)
return PreconditionResult.FromSuccess();
return PreconditionResult.FromError("This command can only be used in NSFW channels.");
}
catch (Exception ex)
{
return PreconditionResult.FromError($"Error checking channel: {ex.Message}");
}
}
}
// Usage:
[Command("nsfw")]
[RequireNsfw]
public async Task NsfwCommand()
{
await ReplyAsync("NSFW content here");
}
Limit command usage per guild rather than per user:
using System.Collections.Concurrent;
public class GuildRateLimitAttribute : PreconditionAttribute
{
private static readonly ConcurrentDictionary<(ulong GuildId, string Command), (DateTime Time, int Count)> _usage = new();
private readonly int _maxUses;
private readonly int _periodSeconds;
public GuildRateLimitAttribute(int maxUses, int periodSeconds)
{
_maxUses = maxUses;
_periodSeconds = periodSeconds;
}
public override Task CheckPermissionsAsync(
CommandContext context,
CommandInfo command,
IServiceProvider? services)
{
if (!context.GuildId.HasValue)
return Task.FromResult(PreconditionResult.FromSuccess());
var key = (context.GuildId.Value, command.Name);
var now = DateTime.UtcNow;
if (_usage.TryGetValue(key, out var entry))
{
var timeSinceFirst = (now - entry.Time).TotalSeconds;
if (timeSinceFirst < _periodSeconds)
{
if (entry.Count >= _maxUses)
{
var resetIn = _periodSeconds - timeSinceFirst;
return Task.FromResult(PreconditionResult.FromError(
$"This guild has reached the rate limit. Try again in {resetIn:F0} seconds."));
}
_usage[key] = (entry.Time, entry.Count + 1);
}
else
{
_usage[key] = (now, 1);
}
}
else
{
_usage[key] = (now, 1);
}
return Task.FromResult(PreconditionResult.FromSuccess());
}
}
// Usage:
[Command("poll")]
[GuildRateLimit(5, 3600)] // Max 5 polls per hour per guild
public async Task PollCommand([Remainder] string question)
{
await ReplyAsync($"Poll created: {question}");
}
[Command("admin")]
[RequireOwner]
[Cooldown(60)]
public async Task AdminCommand()
{
await ReplyAsync("Admin action performed");
}
Apply preconditions to all commands in a module:
[RequireContext(ContextType.Guild)]
public class ModerationCommands : ModuleBase
{
// All commands in this module require guild context
[Command("kick")]
[RequireUserPermission(Permissions.KickMembers)]
public async Task KickCommand(ulong userId)
{
// Implementation
}
[Command("ban")]
[RequireUserPermission(Permissions.BanMembers)]
public async Task BanCommand(ulong userId)
{
// Implementation
}
}
Commands can have multiple preconditions - all must pass:
[Command("premium-admin")]
[RequireOwner]
[RequirePremium]
[RequireContext(ContextType.Guild)]
[Cooldown(300)]
public async Task PremiumAdminCommand()
{
// User must be owner, have premium, be in a guild, and respect cooldown
}
Preconditions can access services through the IServiceProvider parameter:
// Setup in Program.cs
var services = new ServiceCollection()
.AddSingleton()
.AddSingleton()
.BuildServiceProvider();
var commands = new CommandService(
prefixChar: '/',
logger: Log.Logger,
services: services // Pass services here
);
// In your precondition
public class CustomPreconditionAttribute : PreconditionAttribute
{
public override async Task CheckPermissionsAsync(
CommandContext context,
CommandInfo command,
IServiceProvider? services)
{
var database = services?.GetService(typeof(IDatabaseService)) as IDatabaseService;
var config = services?.GetService(typeof(IConfigService)) as IConfigService;
// Use services for your checks
if (database != null)
{
var userSettings = await database.GetUserSettingsAsync(context.User.Id);
// Check conditions based on user settings
}
return PreconditionResult.FromSuccess();
}
}
When a precondition fails, the CommandService returns an error result:
var result = await commands.ExecuteAsync(context, argPos);
if (!result.IsSuccess && result.ErrorType == CommandError.UnmetPrecondition)
{
await api.SendMessage(context.ChannelId, new()
{
Content = $"⛔ {result.Error}"
});
}
// Good
return PreconditionResult.FromError(
"You need the 'Manage Messages' permission to use this command.");
// Bad
return PreconditionResult.FromError("No permission");
// Good: Actually await API calls
public override async Task CheckPermissionsAsync(...)
{
var member = await context.Client.GetGuildMember(guildId, userId);
return PreconditionResult.FromSuccess();
}
// Bad: Not awaiting
public override Task CheckPermissionsAsync(...)
{
var member = context.Client.GetGuildMember(guildId, userId); // Not awaited!
return Task.FromResult(PreconditionResult.FromSuccess());
}
public override async Task CheckPermissionsAsync(...)
{
try
{
// Your logic that might throw
var data = await context.Client.GetSomeData();
return PreconditionResult.FromSuccess();
}
catch (Exception ex)
{
// Return error instead of letting exception propagate
return PreconditionResult.FromError($"Failed to check precondition: {ex.Message}");
}
}
// Cache data when possible
private static readonly ConcurrentDictionary _cache = new();
// Clean up old cache entries periodically
private void CleanupCache()
{
var old = DateTime.UtcNow.AddHours(-1);
var expiredKeys = _cache.Where(kvp => kvp.Value < old).Select(kvp => kvp.Key);
foreach (var key in expiredKeys)
_cache.TryRemove(key, out _);
}
// Good: Parameterized and reusable
public class MinimumAccountAgeAttribute : PreconditionAttribute
{
private readonly int _days;
public MinimumAccountAgeAttribute(int days)
{
_days = days;
}
// Implementation...
}
// Usage:
[MinimumAccountAge(7)] // 7 days
[MinimumAccountAge(30)] // 30 days
using Fluxer.Net.Commands;
using Fluxer.Net.Commands.Attributes;
using Fluxer.Net.Data.Enums;
[RequireContext(ContextType.Guild)]
public class ModerationModule : ModuleBase
{
[Command("warn")]
[RequireUserPermission(Permissions.ManageMessages)]
[Cooldown(5)]
public async Task WarnCommand(ulong userId, [Remainder] string reason)
{
await ReplyAsync($"⚠️ Warned <@{userId}> for: {reason}");
}
[Command("kick")]
[RequireUserPermission(Permissions.KickMembers)]
[RequireBotPermission(Permissions.KickMembers)]
[Cooldown(10)]
public async Task KickCommand(ulong userId, [Remainder] string reason = "No reason provided")
{
await Context.Client.KickGuildMember(Context.GuildId.Value, userId);
await ReplyAsync($"👢 Kicked <@{userId}>. Reason: {reason}");
}
[Command("ban")]
[RequireUserPermission(Permissions.BanMembers)]
[RequireBotPermission(Permissions.BanMembers)]
[Cooldown(30)]
public async Task BanCommand(ulong userId, [Remainder] string reason = "No reason provided")
{
await Context.Client.BanGuildMember(Context.GuildId.Value, userId, reason);
await ReplyAsync($"🔨 Banned <@{userId}>. Reason: {reason}");
}
[Command("purge")]
[RequireUserPermission(Permissions.ManageMessages)]
[RequireBotPermission(Permissions.ManageMessages)]
[GuildRateLimit(10, 60)] // Max 10 purges per minute per guild
public async Task PurgeCommand(int count)
{
if (count < 1 || count > 100)
{
await ReplyAsync("Please specify a number between 1 and 100.");
return;
}
// Implementation...
await ReplyAsync($"🗑️ Purged {count} messages");
}
}