mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-24 17:34:13 +00:00
Implemented more commands
This commit is contained in:
parent
1bf87f8827
commit
74f876c4af
5 changed files with 630 additions and 14 deletions
|
@ -45,9 +45,9 @@ internal abstract class BotApplicationCommand {
|
||||||
/// throwing a FormatException if the input is not recognized.
|
/// throwing a FormatException if the input is not recognized.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected static string ParseTimeZone(string tzinput) {
|
protected static string ParseTimeZone(string tzinput) {
|
||||||
if (!TzNameMap.TryGetValue(tzinput, out string? tz)) throw new FormatException(":x: Unexpected time zone name."
|
if (!TzNameMap.TryGetValue(tzinput, out string? tz))
|
||||||
+ $" Refer to `INSERT COMMAND NAME HERE` to help determine the correct value."); // TODO fix!!!!!!!!!!!!!!!!!!!
|
throw new FormatException(":x: Unknown time zone name.\n" +
|
||||||
// put link to tz finder -and- refer to command for elaborate info
|
"To find your time zone, please refer to: https://kevinnovak.github.io/Time-Zone-Picker/");
|
||||||
return tz!;
|
return tz!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,23 @@ internal class HelpCommands : BotApplicationCommand {
|
||||||
static HelpCommands() {
|
static HelpCommands() {
|
||||||
_helpEmbedRegCommandsField = new EmbedFieldBuilder() {
|
_helpEmbedRegCommandsField = new EmbedFieldBuilder() {
|
||||||
Name = "Commands",
|
Name = "Commands",
|
||||||
Value = $"`/set-birthday` - {RegistrationCommands.HelpSet}\n"
|
Value = $"`/set-birthday` - {RegistrationCommands.HelpSet}\n" +
|
||||||
+ $"`/set-timezone` - {RegistrationCommands.HelpZone}\n"
|
$"`/set-timezone` - {RegistrationCommands.HelpZone}\n" +
|
||||||
+ $"`/remove-timezone` - {RegistrationCommands.HelpZoneDel}\n"
|
$"`/remove-timezone` - {RegistrationCommands.HelpZoneDel}\n" +
|
||||||
+ $"`/remove-birthday` - {RegistrationCommands.HelpDel}"
|
$"`/remove-birthday` - {RegistrationCommands.HelpDel}\n" +
|
||||||
|
$"`/birthday` - {QueryCommands.HelpBirthdayFor}\n" +
|
||||||
|
$"`/recent`, `/upcoming` - {QueryCommands.HelpRecentUpcoming}"
|
||||||
|
|
||||||
};
|
};
|
||||||
_helpEmbedModCommandsField = new EmbedFieldBuilder() {
|
_helpEmbedModCommandsField = new EmbedFieldBuilder() {
|
||||||
Name = "Moderator commands",
|
Name = "Moderator commands",
|
||||||
Value = $"`/override` - {RegistrationOverrideCommands.HelpOverride}"
|
Value =
|
||||||
|
$"`/config` - {ModCommands.HelpConfig}\n" +
|
||||||
|
$"`/announce` - {ModCommands.HelpConfAnnounce}\n" +
|
||||||
|
$"`/blocking` - {ModCommands.HelpConfBlocking}\n" +
|
||||||
|
$"`/list-all` - {QueryCommands.HelpListAll}\n" +
|
||||||
|
$"`/override` - {RegistrationOverrideCommands.HelpOverride}\n" +
|
||||||
|
$"See also: `/config help`, `/announce help`, `/blocking help`."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,10 +49,10 @@ internal class HelpCommands : BotApplicationCommand {
|
||||||
.WithAuthor("Help & About")
|
.WithAuthor("Help & About")
|
||||||
.WithFooter($"Birthday Bot {ver} - Shard {instance.ShardId:00} up {Program.BotUptime}",
|
.WithFooter($"Birthday Bot {ver} - Shard {instance.ShardId:00} up {Program.BotUptime}",
|
||||||
instance.DiscordClient.CurrentUser.GetAvatarUrl())
|
instance.DiscordClient.CurrentUser.GetAvatarUrl())
|
||||||
.WithDescription("Support, data policy, etc: https://noithecat.dev/bots/BirthdayBot\n"
|
.WithDescription("Thank you for using Birthday Bot!\n" +
|
||||||
+ "This bot is provided for free, without any paywalls or exclusive paid features. If this bot has been useful to you, "
|
"Support, data policy, etc: https://noithecat.dev/bots/BirthdayBot\n" +
|
||||||
+ "please consider taking a look at the author's Ko-fi: https://ko-fi.com/noithecat.\n"
|
"This bot is provided for free, without any paywalls or exclusive paid features. If this bot has been useful to you, " +
|
||||||
+ "Thank you for using Birthday Bot!")
|
"please consider taking a look at the author's Ko-fi: https://ko-fi.com/noithecat.")
|
||||||
.AddField(_helpEmbedRegCommandsField)
|
.AddField(_helpEmbedRegCommandsField)
|
||||||
.AddField(_helpEmbedModCommandsField)
|
.AddField(_helpEmbedModCommandsField)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
317
ApplicationCommands/ModCommands.cs
Normal file
317
ApplicationCommands/ModCommands.cs
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
using BirthdayBot.Data;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BirthdayBot.ApplicationCommands;
|
||||||
|
|
||||||
|
internal class ModCommands : BotApplicationCommand {
|
||||||
|
private readonly ShardManager _instance;
|
||||||
|
|
||||||
|
private delegate Task SubCommandHandler(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam);
|
||||||
|
private static Embed HelpSubAnnounceEmbed { get; } = new EmbedBuilder()
|
||||||
|
.AddField("Subcommands for `/announce`",
|
||||||
|
$"`channel` - {HelpSAnChannel} {HelpPofxBlankUnset}\n" +
|
||||||
|
$"`ping` - {HelpSAnPing}\n" +
|
||||||
|
$"`message-single` - {HelpSAnSingle}\n" +
|
||||||
|
$"`message-multi` - {HelpSAnMulti}")
|
||||||
|
.AddField("Custom announcement messages",
|
||||||
|
"The `message-single` and `message-multi` subcommands allow moderators to edit the message sent into the announcement " +
|
||||||
|
"channel.\nThe first command `message-single` sets the message that is displayed when *one* user has a birthday. The second " +
|
||||||
|
"command `message-multi` sets the message used when *two or more* users have birthdays. If only one of the two messages " +
|
||||||
|
"have been set, this bot will use the same message in both cases.\n\n" +
|
||||||
|
"For further customization, you may use the token `%n` in your message to specify where the name(s) should appear.\n")
|
||||||
|
.Build();
|
||||||
|
private static Embed HelpSubBlockingEmbed { get; } = new EmbedBuilder()
|
||||||
|
.AddField("Commands", "testtesttest").Build();
|
||||||
|
|
||||||
|
public const string HelpConfig = "Configure for essential bot settings.";
|
||||||
|
public const string HelpConfAnnounce = "Configuration regarding announcement messages.";
|
||||||
|
public const string HelpConfBlocking = "Configuration regarding limiting user access.";
|
||||||
|
const string HelpPofxBlankUnset = " Leave blank to unset.";
|
||||||
|
const string HelpOptChannelDefault = "The corresponding channel to use.";
|
||||||
|
const string HelpOptRoleDefault = "The corresponding role to use.";
|
||||||
|
private const string HelpSAnChannel = "Set the channel which to send birthday announcements.";
|
||||||
|
private const string HelpSAnPing = "Set whether to ping users mentioned in the announcement.";
|
||||||
|
private const string HelpSAnSingle = "Set the message announced when one user has a birthday.";
|
||||||
|
private const string HelpSAnMulti = "Set the message announced when two or more users have a birthday.";
|
||||||
|
|
||||||
|
public ModCommands(ShardManager instance) => _instance = instance;
|
||||||
|
|
||||||
|
public override IEnumerable<ApplicationCommandProperties> GetCommands() => new ApplicationCommandProperties[] {
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.WithName("config")
|
||||||
|
.WithDescription(HelpPfxModOnly + HelpConfig)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("birthday-role")
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.WithDescription(HelpPfxModOnly + "Set or modify the role given to those having a birthday.")
|
||||||
|
.AddOption("role", ApplicationCommandOptionType.Role, HelpOptRoleDefault, isRequired: true)
|
||||||
|
)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("mod-role")
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.WithDescription(HelpPfxModOnly + "Allow a role to be able to use moderator commands." + HelpPofxBlankUnset)
|
||||||
|
.AddOption("role", ApplicationCommandOptionType.Role, HelpOptRoleDefault, isRequired: false)
|
||||||
|
)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("server-timezone")
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.WithDescription(HelpPfxModOnly + "Set the default time zone to be used in this server." + HelpPofxBlankUnset)
|
||||||
|
.AddOption("zone", ApplicationCommandOptionType.String, HelpOptZone, isRequired: false)
|
||||||
|
)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("check")
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.WithDescription(HelpPfxModOnly + "Give a configuration status report.")
|
||||||
|
)
|
||||||
|
.Build(),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.WithName("announce")
|
||||||
|
.WithDescription(HelpPfxModOnly + HelpConfAnnounce)
|
||||||
|
.AddOption("help", ApplicationCommandOptionType.SubCommand,
|
||||||
|
HelpPfxModOnly + "Display information regarding announcement messages.")
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("channel")
|
||||||
|
.WithDescription(HelpPfxModOnly + HelpSAnChannel + HelpPofxBlankUnset)
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.AddOption("channel", ApplicationCommandOptionType.Channel, HelpOptChannelDefault, isRequired: false)
|
||||||
|
)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("ping")
|
||||||
|
.WithDescription(HelpPfxModOnly + HelpSAnPing)
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.AddOption("option", ApplicationCommandOptionType.Boolean,
|
||||||
|
"True to ping users or False to display names normally.", isRequired: true)
|
||||||
|
)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("message-single")
|
||||||
|
.WithDescription(HelpPfxModOnly + HelpSAnSingle)
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.AddOption("message", ApplicationCommandOptionType.String, "The new message to use.")
|
||||||
|
)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("message-multi")
|
||||||
|
.WithDescription(HelpPfxModOnly + HelpSAnMulti)
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.AddOption("message", ApplicationCommandOptionType.String, "The new message to use.")
|
||||||
|
)
|
||||||
|
.Build(),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.WithName("blocking")
|
||||||
|
.WithDescription(HelpPfxModOnly + HelpConfBlocking)
|
||||||
|
.AddOption("help", ApplicationCommandOptionType.SubCommand,
|
||||||
|
HelpPfxModOnly + "Display information regarding user blocking.")
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("moderated")
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.WithDescription(HelpPfxModOnly + "Set moderated mode on the server.")
|
||||||
|
.AddOption("enable", ApplicationCommandOptionType.Boolean,
|
||||||
|
"True to enable moderated mode, False to disable.", isRequired: true)
|
||||||
|
)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("block-user")
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.WithDescription(HelpPfxModOnly + "Add a user to the blocklist.")
|
||||||
|
.AddOption("user", ApplicationCommandOptionType.User, "The user to add to the blocklist.", isRequired: true)
|
||||||
|
)
|
||||||
|
.AddOption(new SlashCommandOptionBuilder()
|
||||||
|
.WithName("unblock-user")
|
||||||
|
.WithType(ApplicationCommandOptionType.SubCommand)
|
||||||
|
.WithDescription(HelpPfxModOnly + "Remove a user from the blocklist.")
|
||||||
|
.AddOption("user", ApplicationCommandOptionType.User, "The user to remove from the blocklist.", isRequired: true)
|
||||||
|
)
|
||||||
|
.Build()
|
||||||
|
};
|
||||||
|
public override CommandResponder? GetHandlerFor(string commandName) => commandName switch {
|
||||||
|
"config" => CmdConfigDispatch,
|
||||||
|
"announce" => CmdConfigDispatch,
|
||||||
|
"blocking" => CmdConfigDispatch,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
private Task CmdConfigDispatch(ShardInstance instance, GuildConfiguration gconf, SocketSlashCommand arg) {
|
||||||
|
if (!gconf.IsBotModerator((SocketGuildUser)arg.User)) return arg.RespondAsync(ErrNotAllowed);
|
||||||
|
|
||||||
|
var name = arg.Data.Options.First().Name;
|
||||||
|
if (name == "help") return HelpCommandHandler(arg, arg.CommandName);
|
||||||
|
|
||||||
|
SubCommandHandler? subh = arg.Data.Options.First().Name switch {
|
||||||
|
"birthday-role" => CmdConfigSubBRole,
|
||||||
|
"mod-role" => CmdConfigSubMRole,
|
||||||
|
"server-timezone" => CmdConfigSubTz,
|
||||||
|
"check" => CmdConfigSubCheck,
|
||||||
|
"channel" => CmdAnnounceSubChannel,
|
||||||
|
"ping" => CmdAnnounceSubPing,
|
||||||
|
"message-single" => CmdAnnounceSubMsg,
|
||||||
|
"message-multi" => CmdAnnounceSubMsg,
|
||||||
|
"moderated" => CmdBlockSubModerated,
|
||||||
|
"block-user" => CmdBlockSubAddDel,
|
||||||
|
"unblock-user" => CmdBlockSubAddDel,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (subh == null) return arg.RespondAsync(ShardInstance.UnknownCommandError, ephemeral: true);
|
||||||
|
|
||||||
|
var subparam = ((SocketSlashCommandDataOption)arg.Data.Options.First()).Options.ToDictionary(o => o.Name, o => o.Value);
|
||||||
|
return subh(gconf, arg, subparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HelpCommandHandler(SocketSlashCommand arg, string baseCommand) {
|
||||||
|
var answer = baseCommand switch {
|
||||||
|
"announce" => HelpSubAnnounceEmbed,
|
||||||
|
"blocking" => HelpSubBlockingEmbed,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
if (answer == null) {
|
||||||
|
await arg.RespondAsync(ShardInstance.UnknownCommandError, ephemeral: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await arg.RespondAsync(embed: answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdConfigSubBRole(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
var role = (SocketRole)subparam["role"];
|
||||||
|
gconf.RoleId = role.Id;
|
||||||
|
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||||
|
await arg.RespondAsync($":white_check_mark: The birthday role has been set to **{role.Name}**.").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdConfigSubMRole(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
var role = subparam.GetValueOrDefault("role") as SocketRole;
|
||||||
|
gconf.ModeratorRole = role?.Id;
|
||||||
|
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||||
|
await arg.RespondAsync(":white_check_mark: The moderator role has been " +
|
||||||
|
(role == null ? "unset." : $"set to **{role.Name}**."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdConfigSubTz(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
const string Response = ":white_check_mark: The server's time zone has been ";
|
||||||
|
var inputtz = subparam.GetValueOrDefault("zone") as string;
|
||||||
|
|
||||||
|
if (inputtz == null) {
|
||||||
|
gconf.TimeZone = null;
|
||||||
|
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||||
|
await arg.RespondAsync(Response + "unset.").ConfigureAwait(false);
|
||||||
|
} else {
|
||||||
|
string zone;
|
||||||
|
try {
|
||||||
|
zone = ParseTimeZone(inputtz);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
arg.RespondAsync(e.Message).Wait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gconf.TimeZone = zone;
|
||||||
|
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||||
|
await arg.RespondAsync(Response + $"set to **{zone}**.").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdConfigSubCheck(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
static string DoTestFor(string label, Func<bool> test) => $"{label}: { (test() ? ":white_check_mark: Yes" : ":x: No") }";
|
||||||
|
var result = new StringBuilder();
|
||||||
|
SocketTextChannel channel = (SocketTextChannel)arg.Channel;
|
||||||
|
var guild = channel.Guild;
|
||||||
|
var conf = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
||||||
|
var userbdays = await GuildUserConfiguration.LoadAllAsync(guild.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
result.AppendLine($"Server ID: `{guild.Id}` | Bot shard ID: `{_instance.GetShardIdFor(guild.Id):00}`");
|
||||||
|
result.AppendLine($"Number of registered birthdays: `{ userbdays.Count() }`");
|
||||||
|
result.AppendLine($"Server time zone: `{ (conf?.TimeZone ?? "Not set - using UTC") }`");
|
||||||
|
result.AppendLine();
|
||||||
|
|
||||||
|
bool hasMembers = Common.HasMostMembersDownloaded(guild);
|
||||||
|
result.Append(DoTestFor("Bot has obtained the user list", () => hasMembers));
|
||||||
|
result.AppendLine($" - Has `{guild.DownloadedMemberCount}` of `{guild.MemberCount}` members.");
|
||||||
|
int bdayCount = -1;
|
||||||
|
result.Append(DoTestFor("Birthday processing", delegate {
|
||||||
|
if (!hasMembers) return false;
|
||||||
|
bdayCount = BackgroundServices.BirthdayRoleUpdate.GetGuildCurrentBirthdays(userbdays, conf?.TimeZone).Count;
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
if (hasMembers) result.AppendLine($" - `{bdayCount}` user(s) currently having a birthday.");
|
||||||
|
else result.AppendLine(" - Previous step failed.");
|
||||||
|
result.AppendLine();
|
||||||
|
|
||||||
|
result.AppendLine(DoTestFor("Birthday role set with `bb.config role`", delegate {
|
||||||
|
if (conf == null) return false;
|
||||||
|
SocketRole? role = guild.GetRole(conf.RoleId ?? 0);
|
||||||
|
return role != null;
|
||||||
|
}));
|
||||||
|
result.AppendLine(DoTestFor("Birthday role can be managed by bot", delegate {
|
||||||
|
if (conf == null) return false;
|
||||||
|
SocketRole? role = guild.GetRole(conf.RoleId ?? 0);
|
||||||
|
if (role == null) return false;
|
||||||
|
return guild.CurrentUser.GuildPermissions.ManageRoles && role.Position < guild.CurrentUser.Hierarchy;
|
||||||
|
}));
|
||||||
|
result.AppendLine();
|
||||||
|
|
||||||
|
SocketTextChannel? announcech = null;
|
||||||
|
result.AppendLine(DoTestFor("(Optional) Announcement channel set with `bb.config channel`", delegate {
|
||||||
|
if (conf == null) return false;
|
||||||
|
announcech = guild.GetTextChannel(conf.AnnounceChannelId ?? 0);
|
||||||
|
return announcech != null;
|
||||||
|
}));
|
||||||
|
string disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>";
|
||||||
|
result.AppendLine(DoTestFor($"(Optional) Bot can send messages into { disp }", delegate {
|
||||||
|
if (announcech == null) return false;
|
||||||
|
return guild.CurrentUser.GetPermissions(announcech).SendMessages;
|
||||||
|
}));
|
||||||
|
|
||||||
|
await arg.RespondAsync(embed: new EmbedBuilder() {
|
||||||
|
Author = new EmbedAuthorBuilder() { Name = "Status and config check" },
|
||||||
|
Description = result.ToString()
|
||||||
|
}.Build()).ConfigureAwait(false);
|
||||||
|
|
||||||
|
const int announceMsgPreviewLimit = 350;
|
||||||
|
static string prepareAnnouncePreview(string announce) {
|
||||||
|
string trunc = announce.Length > announceMsgPreviewLimit ? announce[..announceMsgPreviewLimit] + "`(...)`" : announce;
|
||||||
|
var result = new StringBuilder();
|
||||||
|
foreach (var line in trunc.Split('\n'))
|
||||||
|
result.AppendLine($"> {line}");
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
if (conf != null && (conf.AnnounceMessages.Item1 != null || conf.AnnounceMessages.Item2 != null)) {
|
||||||
|
var em = new EmbedBuilder().WithAuthor(new EmbedAuthorBuilder() { Name = "Custom announce messages:" });
|
||||||
|
var dispAnnounces = new StringBuilder("Custom announcement message(s):\n");
|
||||||
|
if (conf.AnnounceMessages.Item1 != null) {
|
||||||
|
em = em.AddField("Single", prepareAnnouncePreview(conf.AnnounceMessages.Item1));
|
||||||
|
}
|
||||||
|
if (conf.AnnounceMessages.Item2 != null) {
|
||||||
|
em = em.AddField("Multi", prepareAnnouncePreview(conf.AnnounceMessages.Item2));
|
||||||
|
}
|
||||||
|
await channel.SendMessageAsync(embed: em.Build()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdAnnounceSubChannel(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
var channel = subparam.GetValueOrDefault("channel") as SocketTextChannel;
|
||||||
|
gconf.AnnounceChannelId = channel?.Id;
|
||||||
|
await gconf.UpdateAsync();
|
||||||
|
await arg.RespondAsync(":white_check_mark: The announcement channel has been " +
|
||||||
|
(channel == null ? "unset." : $"set to **{channel.Name}**."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdAnnounceSubPing(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
var setting = (bool)subparam["option"];
|
||||||
|
gconf.AnnouncePing = setting;
|
||||||
|
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||||
|
await arg.RespondAsync(":white_check_mark: Announcement pings are now " + (setting ? "**on**." : "**off**.")).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdAnnounceSubMsg(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
// Handles "message-single" and "message-multi" subcommands
|
||||||
|
await arg.RespondAsync("unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdBlockSubModerated(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
var setting = (bool)subparam["option"];
|
||||||
|
gconf.IsModerated = setting;
|
||||||
|
await gconf.UpdateAsync().ConfigureAwait(false);
|
||||||
|
await arg.RespondAsync(":white_check_mark: Moderated mode is now " + (setting ? "**on**." : "**off**.")).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdBlockSubAddDel(GuildConfiguration gconf, SocketSlashCommand arg, Dictionary<string, object> subparam) {
|
||||||
|
// Handles "block-user" and "unblock-user" subcommands
|
||||||
|
await arg.RespondAsync("unimplemented");
|
||||||
|
}
|
||||||
|
}
|
281
ApplicationCommands/QueryCommands.cs
Normal file
281
ApplicationCommands/QueryCommands.cs
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
using BirthdayBot.Data;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BirthdayBot.ApplicationCommands;
|
||||||
|
|
||||||
|
internal class QueryCommands : BotApplicationCommand {
|
||||||
|
public const string HelpBirthdayFor = "Gets a user's birthday.";
|
||||||
|
public const string HelpListAll = "Show a full list of all known birthdays.";
|
||||||
|
public const string HelpRecentUpcoming = "Get a list of users who recently had or will have a birthday.";
|
||||||
|
|
||||||
|
public override IEnumerable<ApplicationCommandProperties> GetCommands() => new ApplicationCommandProperties[] {
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.WithName("birthday")
|
||||||
|
.WithDescription(HelpBirthdayFor)
|
||||||
|
.AddOption("user", ApplicationCommandOptionType.User, "The user whose birthday to check.", isRequired: false)
|
||||||
|
.Build(),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.WithName("recent")
|
||||||
|
.WithDescription(HelpRecentUpcoming)
|
||||||
|
.Build(),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.WithName("upcoming")
|
||||||
|
.WithDescription(HelpRecentUpcoming)
|
||||||
|
.Build(),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.WithName("list-all")
|
||||||
|
.WithDescription(HelpPfxModOnly + HelpRecentUpcoming)
|
||||||
|
.AddOption("as-csv", ApplicationCommandOptionType.Boolean, "Whether to output the list in CSV format.")
|
||||||
|
.Build(),
|
||||||
|
};
|
||||||
|
public override CommandResponder? GetHandlerFor(string commandName) => commandName switch {
|
||||||
|
"birthday-for" => CmdBirthdayFor,
|
||||||
|
"recent" => CmdRecentUpcoming,
|
||||||
|
"upcoming" => CmdRecentUpcoming,
|
||||||
|
"list-all" => CmdListAll,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task CmdBirthdayFor(ShardInstance instance, GuildConfiguration gconf, SocketSlashCommand arg) {
|
||||||
|
var searchtarget = arg.Data.Options.FirstOrDefault()?.Value as SocketGuildUser ?? (SocketGuildUser)arg.User;
|
||||||
|
var targetdata = await GuildUserConfiguration.LoadAsync(gconf.GuildId, searchtarget.Id);
|
||||||
|
|
||||||
|
if (!targetdata.IsKnown) {
|
||||||
|
await arg.RespondAsync($"{Common.FormatName(searchtarget, false)} does not have their birthday registered.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await arg.RespondAsync($"{Common.FormatName(searchtarget, false)}: " +
|
||||||
|
$"`{targetdata.BirthDay:00}-{Common.MonthNames[targetdata.BirthMonth]}`" +
|
||||||
|
(targetdata.TimeZone == null ? "" : $" - {targetdata.TimeZone}")).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Recent and upcoming birthdays"
|
||||||
|
// The 'recent' bit removes time zone ambiguity and spares us from extra time zone processing here
|
||||||
|
// TODO stop being lazy
|
||||||
|
private async Task CmdRecentUpcoming(ShardInstance instance, GuildConfiguration gconf, SocketSlashCommand arg) {
|
||||||
|
var guild = ((SocketGuildChannel)arg.Channel).Guild;
|
||||||
|
if (!await HasMemberCacheAsync(guild).ConfigureAwait(false)) {
|
||||||
|
await arg.RespondAsync(MemberCacheEmptyError, ephemeral: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
var search = DateIndex(now.Month, now.Day) - 8; // begin search 8 days prior to current date UTC
|
||||||
|
if (search <= 0) search = 366 - Math.Abs(search);
|
||||||
|
|
||||||
|
var query = await GetSortedUsersAsync(guild).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// TODO pagination instead of this workaround
|
||||||
|
bool hasOutputOneLine = false;
|
||||||
|
// First output is shown as an interaction response, followed then as regular channel messages
|
||||||
|
async Task doOutput(string msg) {
|
||||||
|
if (!hasOutputOneLine) {
|
||||||
|
await arg.RespondAsync(msg).ConfigureAwait(false);
|
||||||
|
hasOutputOneLine = true;
|
||||||
|
} else {
|
||||||
|
await arg.Channel.SendMessageAsync(msg).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = new StringBuilder();
|
||||||
|
var resultCount = 0;
|
||||||
|
output.AppendLine("Recent and upcoming birthdays:");
|
||||||
|
for (int count = 0; count <= 21; count++) // cover 21 days total (7 prior, current day, 14 upcoming)
|
||||||
|
{
|
||||||
|
var results = from item in query
|
||||||
|
where item.DateIndex == search
|
||||||
|
select item;
|
||||||
|
|
||||||
|
// push up search by 1 now, in case we back out early
|
||||||
|
search += 1;
|
||||||
|
if (search > 366) search = 1; // wrap to beginning of year
|
||||||
|
|
||||||
|
if (!results.Any()) continue; // back out early
|
||||||
|
resultCount += results.Count();
|
||||||
|
|
||||||
|
// Build sorted name list
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var item in results) {
|
||||||
|
names.Add(item.DisplayName);
|
||||||
|
}
|
||||||
|
names.Sort(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var first = true;
|
||||||
|
output.AppendLine();
|
||||||
|
output.Append($"● `{Common.MonthNames[results.First().BirthMonth]}-{results.First().BirthDay:00}`: ");
|
||||||
|
foreach (var item in names) {
|
||||||
|
// If the output is starting to fill up, send out this message and prepare a new one.
|
||||||
|
if (output.Length > 800) {
|
||||||
|
await doOutput(output.ToString()).ConfigureAwait(false);
|
||||||
|
output.Clear();
|
||||||
|
first = true;
|
||||||
|
output.Append($"● `{Common.MonthNames[results.First().BirthMonth]}-{results.First().BirthDay:00}`: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first) first = false;
|
||||||
|
else output.Append(", ");
|
||||||
|
output.Append(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultCount == 0)
|
||||||
|
await arg.RespondAsync(
|
||||||
|
"There are no recent or upcoming birthdays (within the last 7 days and/or next 21 days).")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
await doOutput(output.ToString()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CmdListAll(ShardInstance instance, GuildConfiguration gconf, SocketSlashCommand arg) {
|
||||||
|
var guild = ((SocketGuildChannel)arg.Channel).Guild;
|
||||||
|
// For now, we're restricting this command to moderators only. This may turn into an option later.
|
||||||
|
if (!gconf.IsBotModerator((SocketGuildUser)arg.User)) {
|
||||||
|
// Do not add detailed usage information to this error message.
|
||||||
|
await arg.RespondAsync(":x: Only bot moderators may use this command.").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await HasMemberCacheAsync(guild)) {
|
||||||
|
await arg.RespondAsync(MemberCacheEmptyError).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for CSV option
|
||||||
|
var useCsv = arg.Data.Options.FirstOrDefault()?.Value as bool? ?? false;
|
||||||
|
|
||||||
|
var bdlist = await GetSortedUsersAsync(guild).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var filepath = Path.GetTempPath() + "birthdaybot-" + guild.Id;
|
||||||
|
string fileoutput;
|
||||||
|
if (useCsv) {
|
||||||
|
fileoutput = ListExportCsv(guild, bdlist);
|
||||||
|
filepath += ".csv";
|
||||||
|
} else {
|
||||||
|
fileoutput = ListExportNormal(guild, bdlist);
|
||||||
|
filepath += ".txt.";
|
||||||
|
}
|
||||||
|
await File.WriteAllTextAsync(filepath, fileoutput, Encoding.UTF8).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await arg.RespondWithFileAsync(filepath, "birthdaybot-" + guild.Id + (useCsv ? ".csv" : ".txt"),
|
||||||
|
$"Exported {bdlist.Count} birthdays to file.",
|
||||||
|
null, false, false, null, null, null, null);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Program.Log("Listing", ex.ToString());
|
||||||
|
} finally {
|
||||||
|
File.Delete(filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches all guild birthdays and places them into an easily usable structure.
|
||||||
|
/// Users currently not in the guild are not included in the result.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<List<ListItem>> GetSortedUsersAsync(SocketGuild guild) {
|
||||||
|
using var db = await Database.OpenConnectionAsync();
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = "select user_id, birth_month, birth_day from " + GuildUserConfiguration.BackingTable
|
||||||
|
+ " where guild_id = @Gid order by birth_month, birth_day";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
||||||
|
c.Prepare();
|
||||||
|
using var r = await c.ExecuteReaderAsync();
|
||||||
|
var result = new List<ListItem>();
|
||||||
|
while (await r.ReadAsync()) {
|
||||||
|
var id = (ulong)r.GetInt64(0);
|
||||||
|
var month = r.GetInt32(1);
|
||||||
|
var day = r.GetInt32(2);
|
||||||
|
|
||||||
|
var guildUser = guild.GetUser(id);
|
||||||
|
if (guildUser == null) continue; // Skip user not in guild
|
||||||
|
|
||||||
|
result.Add(new ListItem() {
|
||||||
|
BirthMonth = month,
|
||||||
|
BirthDay = day,
|
||||||
|
DateIndex = DateIndex(month, day),
|
||||||
|
UserId = guildUser.Id,
|
||||||
|
DisplayName = Common.FormatName(guildUser, false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ListExportNormal(SocketGuild guild, IEnumerable<ListItem> list) {
|
||||||
|
// Output: "● Mon-dd: (user ID) Username [ - Nickname: (nickname)]"
|
||||||
|
var result = new StringBuilder();
|
||||||
|
result.AppendLine("Birthdays in " + guild.Name);
|
||||||
|
result.AppendLine();
|
||||||
|
foreach (var item in list) {
|
||||||
|
var user = guild.GetUser(item.UserId);
|
||||||
|
if (user == null) continue; // User disappeared in the instant between getting list and processing
|
||||||
|
result.Append($"● {Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}: ");
|
||||||
|
result.Append(item.UserId);
|
||||||
|
result.Append(" " + user.Username + "#" + user.Discriminator);
|
||||||
|
if (user.Nickname != null) result.Append(" - Nickname: " + user.Nickname);
|
||||||
|
result.AppendLine();
|
||||||
|
}
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ListExportCsv(SocketGuild guild, IEnumerable<ListItem> list) {
|
||||||
|
// Output: User ID, Username, Nickname, Month-Day, Month, Day
|
||||||
|
var result = new StringBuilder();
|
||||||
|
|
||||||
|
// Conforming to RFC 4180; with header
|
||||||
|
result.Append("UserId,Username,Nickname,MonthDayDisp,Month,Day");
|
||||||
|
result.Append("\r\n"); // crlf line break is specified by the standard
|
||||||
|
foreach (var item in list) {
|
||||||
|
var user = guild.GetUser(item.UserId);
|
||||||
|
if (user == null) continue; // User disappeared in the instant between getting list and processing
|
||||||
|
result.Append(item.UserId);
|
||||||
|
result.Append(',');
|
||||||
|
result.Append(CsvEscape(user.Username + "#" + user.Discriminator));
|
||||||
|
result.Append(',');
|
||||||
|
if (user.Nickname != null) result.Append(user.Nickname);
|
||||||
|
result.Append(',');
|
||||||
|
result.Append($"{Common.MonthNames[item.BirthMonth]}-{item.BirthDay:00}");
|
||||||
|
result.Append(',');
|
||||||
|
result.Append(item.BirthMonth);
|
||||||
|
result.Append(',');
|
||||||
|
result.Append(item.BirthDay);
|
||||||
|
result.Append("\r\n");
|
||||||
|
}
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CsvEscape(string input) {
|
||||||
|
var result = new StringBuilder();
|
||||||
|
result.Append('"');
|
||||||
|
foreach (var ch in input) {
|
||||||
|
if (ch == '"') result.Append('"');
|
||||||
|
result.Append(ch);
|
||||||
|
}
|
||||||
|
result.Append('"');
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DateIndex(int month, int day) {
|
||||||
|
var dateindex = 0;
|
||||||
|
// Add month offsets
|
||||||
|
if (month > 1) dateindex += 31; // Offset January
|
||||||
|
if (month > 2) dateindex += 29; // Offset February (incl. leap day)
|
||||||
|
if (month > 3) dateindex += 31; // etc
|
||||||
|
if (month > 4) dateindex += 30;
|
||||||
|
if (month > 5) dateindex += 31;
|
||||||
|
if (month > 6) dateindex += 30;
|
||||||
|
if (month > 7) dateindex += 31;
|
||||||
|
if (month > 8) dateindex += 31;
|
||||||
|
if (month > 9) dateindex += 30;
|
||||||
|
if (month > 10) dateindex += 31;
|
||||||
|
if (month > 11) dateindex += 30;
|
||||||
|
dateindex += day;
|
||||||
|
return dateindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ListItem {
|
||||||
|
public int DateIndex;
|
||||||
|
public int BirthMonth;
|
||||||
|
public int BirthDay;
|
||||||
|
public ulong UserId;
|
||||||
|
public string DisplayName;
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,10 +71,11 @@ class ShardManager : IDisposable {
|
||||||
foreach (var item in cmdsMods.Commands) _textCommands.Add(item.Item1, item.Item2);
|
foreach (var item in cmdsMods.Commands) _textCommands.Add(item.Item1, item.Item2);
|
||||||
|
|
||||||
_appCommands = new List<BotApplicationCommand>() {
|
_appCommands = new List<BotApplicationCommand>() {
|
||||||
// TODO fill this out
|
|
||||||
new HelpCommands(),
|
new HelpCommands(),
|
||||||
new RegistrationCommands(),
|
new RegistrationCommands(),
|
||||||
new RegistrationOverrideCommands()
|
new RegistrationOverrideCommands(),
|
||||||
|
new QueryCommands(),
|
||||||
|
new ModCommands(this)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Allocate shards based on configuration
|
// Allocate shards based on configuration
|
||||||
|
@ -130,6 +131,14 @@ class ShardManager : IDisposable {
|
||||||
return newInstance;
|
return newInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int? GetShardIdFor(ulong guildId) {
|
||||||
|
foreach (var sh in _shards.Values) {
|
||||||
|
if (sh == null) continue;
|
||||||
|
if (sh.DiscordClient.GetGuild(guildId) != null) return sh.ShardId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
#region Status checking and display
|
#region Status checking and display
|
||||||
private struct GuildStatusData {
|
private struct GuildStatusData {
|
||||||
public int GuildCount;
|
public int GuildCount;
|
||||||
|
|
Loading…
Reference in a new issue