Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
107 changes: 3 additions & 104 deletions Commands/GlobalCmds.cs → Commands/HelpCmds.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using System.Reflection;
using System.Reflection;

namespace Cliptok.Commands
{
public class GlobalCmds
public class HelpCmds
{
// These commands will be registered outside of the home server and can be used anywhere, even in DMs.

// Most of this is taken from DSharpPlus.CommandsNext and adapted to fit here.
// https://github.com/DSharpPlus/DSharpPlus/blob/1c1aa15/DSharpPlus.CommandsNext/CommandsNextExtension.cs#L829
[Command("helptextcmd"), Description("Displays command help.")]
Expand Down Expand Up @@ -221,105 +219,6 @@ await ctx.RespondAsync(
await ctx.RespondAsync(builder);
}

[Command("pingtextcmd")]
[TextAlias("ping")]
[Description("Pong? This command lets you know whether I'm working well.")]
[AllowedProcessors(typeof(TextCommandProcessor))]
public async Task Ping(TextCommandContext ctx)
{
ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString());
DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging...");
ulong ping = (return_message.Id - ctx.Message.Id) >> 22;
char[] choices = new char[] { 'a', 'e', 'o', 'u', 'i', 'y' };
char letter = choices[Program.rand.Next(0, choices.Length)];
await return_message.ModifyAsync($"P{letter}ng! 🏓\n" +
$"• It took me `{ping}ms` to reply to your message!\n" +
$"• Last Websocket Heartbeat took `{Math.Round(ctx.Client.GetConnectionLatency(0).TotalMilliseconds, 0)}ms`!");
}

[Command("userinfo")]
[TextAlias("user-info", "whois")]
[Description("Show info about a user.")]
[AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))]
public async Task UserInfoSlashCommand(CommandContext ctx, [Parameter("user"), Description("The user to retrieve information about.")] DiscordUser user = null, [Parameter("public"), Description("Whether to show the output publicly.")] bool publicMessage = false)
{
if (user is null)
user = ctx.User;

await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage);
}

[Command("remindmetextcmd")]
[Description("Set a reminder for yourself. Example: !reminder 1h do the thing")]
[TextAlias("remindme", "reminder", "rember", "wemember", "remember", "remind")]
[AllowedProcessors(typeof(TextCommandProcessor))]
[RequireHomeserverPerm(ServerPermLevel.Tier4, WorkOutside = true)]
public async Task RemindMe(
TextCommandContext ctx,
[Description("When to trigger the reminder. Accepts many formats. Surround with quotes if you need to use spaces.")] string timetoParse,
[RemainingText, Description("The text to send when the reminder triggers.")] string reminder = "..."
)
{
DateTime t = TimeHelpers.ParseAnyDateFormat(timetoParse);

if (t <= DateTime.UtcNow)
{
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time can't be in the past!");
return;
}
#if !DEBUG
else if (t < (DateTime.UtcNow + TimeSpan.FromSeconds(59)))
{
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time must be at least a minute in the future!");
return;
}
#endif
string guildId;

if (ctx.Channel.IsPrivate)
guildId = "@me";
else
guildId = ctx.Guild.Id.ToString();

var reminderObject = new Reminder()
{
UserID = ctx.User.Id,
ChannelID = ctx.Channel.Id,
MessageID = ctx.Message.Id,
MessageLink = $"https://discord.com/channels/{guildId}/{ctx.Channel.Id}/{ctx.Message.Id}",
ReminderText = reminder,
ReminderTime = t,
OriginalTime = DateTime.UtcNow
};

await Program.redis.ListRightPushAsync("reminders", JsonConvert.SerializeObject(reminderObject));
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} I'll try my best to remind you about that on <t:{TimeHelpers.ToUnixTimestamp(t)}:f> (<t:{TimeHelpers.ToUnixTimestamp(t)}:R>)"); // (In roughly **{TimeHelpers.TimeToPrettyFormat(t.Subtract(ctx.Message.Timestamp.DateTime), false)}**)");
}

public class Reminder
{
[JsonProperty("userID")]
public ulong UserID { get; set; }

[JsonProperty("channelID")]
public ulong ChannelID { get; set; }

[JsonProperty("messageID")]
public ulong MessageID { get; set; }

[JsonProperty("messageLink")]
public string MessageLink { get; set; }

[JsonProperty("reminderText")]
public string ReminderText { get; set; }

[JsonProperty("reminderTime")]
public DateTime ReminderTime { get; set; }

[JsonProperty("originalTime")]
public DateTime OriginalTime { get; set; }
}

// Runs command context checks manually. Returns a list of failed checks.
// Unfortunately DSharpPlus.Commands does not provide a way to execute a command's context checks manually,
// so this will have to do. This may not include all checks, but it includes everything I could think of. -Milkshake
Expand Down Expand Up @@ -410,4 +309,4 @@ private static async Task<IEnumerable<ContextCheckAttribute>> CheckPermissionsAs
return failedChecks;
}
}
}
}
21 changes: 21 additions & 0 deletions Commands/PingCmds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Cliptok.Commands
{
public class PingCmds
{
[Command("pingtextcmd")]
[TextAlias("ping")]
[Description("Pong? This command lets you know whether I'm working well.")]
[AllowedProcessors(typeof(TextCommandProcessor))]
public async Task Ping(TextCommandContext ctx)
{
ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString());
DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging...");
ulong ping = (return_message.Id - ctx.Message.Id) >> 22;
char[] choices = new char[] { 'a', 'e', 'o', 'u', 'i', 'y' };
char letter = choices[Program.rand.Next(0, choices.Length)];
await return_message.ModifyAsync($"P{letter}ng! 🏓\n" +
$"• It took me `{ping}ms` to reply to your message!\n" +
$"• Last Websocket Heartbeat took `{Math.Round(ctx.Client.GetConnectionLatency(0).TotalMilliseconds, 0)}ms`!");
}
}
}
217 changes: 217 additions & 0 deletions Commands/ReminderCmds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
using static Cliptok.Helpers.ReminderHelpers;

namespace Cliptok.Commands
{
public class ReminderCmds
{
// Used to pass context to modal handling
// <user ID, message from context>
public static Dictionary<ulong, DiscordMessage> ReminderInteractionCache = new();

[Command("Remind Me About This")]
[AllowedProcessors(typeof(MessageCommandProcessor))]
[SlashCommandTypes(DiscordApplicationCommandType.MessageContextMenu)]

public static async Task ContextReminder(MessageCommandContext ctx, DiscordMessage targetMessage)
{
await ctx.RespondWithModalAsync(new DiscordModalBuilder()
.WithTitle("Remind Me About This")
.WithCustomId("remind-me-about-this-modal-callback")
.AddTextInput(new DiscordTextInputComponent("remind-me-about-this-time-input"), "When do you want to be reminded?")
);

ReminderInteractionCache[ctx.User.Id] = targetMessage;
}

[Command("reminder")]
[Description("Set, modify and delete reminders.")]
[TextAlias("remindme", "rember", "wemember", "remember", "remind")]
[RequireHomeserverPerm(ServerPermLevel.Tier4, WorkOutside = true)]
[AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))]
public class ReminderCommand
{
[Command("set")]
[Description("Set a reminder.")]
[DefaultGroupCommand]
public static async Task SetReminder(CommandContext ctx,
[Parameter("time"), Description("When do you want to be reminded?")]
string time,
[Parameter("text"), Description("What should the reminder say?")] [MinMaxLength(maxLength: 1000)] [RemainingText]
string text = "")
{
if (ctx is SlashCommandContext)
await ctx.As<SlashCommandContext>().DeferResponseAsync();

var (parsedTime, error) = await ValidateReminderTimeAsync(time);
if (parsedTime is null)
{
await ctx.RespondAsync(error, ephemeral: true);
return;
}

Reminder reminder = new()
{
UserId = ctx.User.Id,
ChannelId = ctx.Channel.Id,
GuildId = ctx.Channel.IsPrivate ? "@me" : ctx.Guild.Id.ToString(),
ReminderId = await GenerateUniqueReminderIdAsync(),
ReminderText = text,
ReminderTime = parsedTime.Value,
SetTime = DateTime.UtcNow
};

var unixTime = ((DateTimeOffset)parsedTime).ToUnixTimeSeconds();

DiscordMessage message;
if (ctx is SlashCommandContext)
{
message = await ctx.As<SlashCommandContext>().FollowupAsync(new DiscordFollowupMessageBuilder()
.WithContent($"{Program.cfgjson.Emoji.Success} I'll try my best to remind you about that on <t:{unixTime}:f> (<t:{unixTime}:R>)"));
reminder.MessageId = message.Id;
}
else
{
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} I'll try my best to remind you about that on <t:{unixTime}:f> (<t:{unixTime}:R>)");
reminder.MessageId = ctx.As<TextCommandContext>().Message.Id;
}

await Program.redis.HashSetAsync("reminders", reminder.ReminderId, JsonConvert.SerializeObject(reminder));
}

[Command("list")]
[Description("List your reminders.")]
[AllowedProcessors(typeof(SlashCommandProcessor))]
public static async Task ListReminders(SlashCommandContext ctx)
{
await ctx.DeferResponseAsync(true);

var userReminders = await GetUserRemindersAsync(ctx.User.Id);

if (userReminders.Count == 0)
{
await ctx.FollowupAsync(new DiscordFollowupMessageBuilder()
.WithContent($"{Program.cfgjson.Emoji.Error} You don't have any reminders!")
.AsEphemeral());
return;
}

await ctx.FollowupAsync(new DiscordFollowupMessageBuilder()
.AddEmbed(await CreateReminderListEmbedAsync(userReminders, ctx.User))
.AsEphemeral());
}

[Command("delete")]
[Description("Delete a reminder.")]
[AllowedProcessors(typeof(SlashCommandProcessor))]
public static async Task DeleteReminder(SlashCommandContext ctx)
{
// We can't defer this!! Want to respond with a modal if the user has >25 reminders.

var userReminders = await GetUserRemindersAsync(ctx.User.Id);
if (userReminders.Count == 0)
{
await ctx.RespondAsync(new DiscordInteractionResponseBuilder()
.WithContent($"{Program.cfgjson.Emoji.Error} You don't have any reminders!")
.AsEphemeral());
return;
}
else if (userReminders.Count <= 25)
{
await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent("Please choose a reminder to delete.")
.AddActionRowComponent(CreateSelectComponentFromReminders(userReminders, "reminder-delete-dropdown-callback"))
.AsEphemeral());
}
else
{
// User has more than 25 reminders. Show a modal where they are prompted to enter the ID for the reminder they want to delete.
// I wanted to paginate a select menu instead, but Discord and D#+ limitations make that really difficult for now. (cba writing my own pagination)

var modalText = "You have a lot of reminders! Please enter the ID of the reminder you wish to delete.";

await ctx.RespondWithModalAsync(new DiscordModalBuilder().WithCustomId("reminder-delete-modal-callback").WithTitle("Delete a Reminder")
.AddTextDisplay(modalText)
.AddTextInput(new DiscordTextInputComponent("reminder-delete-id-input"), "Reminder ID"));
}
}

[Command("modify")]
[Description("Modify a reminder.")]
[AllowedProcessors(typeof(SlashCommandProcessor))]
public static async Task ModifyReminder(SlashCommandContext ctx)
{
// We can't defer this!! Want to respond with a modal if the user has >25 reminders.

var userReminders = await GetUserRemindersAsync(ctx.User.Id);
if (userReminders.Count == 0)
{
await ctx.RespondAsync(new DiscordInteractionResponseBuilder()
.WithContent($"{Program.cfgjson.Emoji.Error} You don't have any reminders!")
.AsEphemeral());
return;
}
else if (userReminders.Count <= 25)
{
await ctx.RespondAsync(
new DiscordInteractionResponseBuilder().WithContent("Please choose a reminder to modify.")
.AddActionRowComponent(CreateSelectComponentFromReminders(userReminders, "reminder-modify-dropdown-callback"))
.AsEphemeral());
}
else
{
// User has more than 25 reminders. Show a modal where they are prompted to enter the ID for the reminder they want to modify.
// I wanted to paginate a select menu instead, but Discord and D#+ limitations make that really difficult for now. (cba writing my own pagination)

var modalText = "You have a lot of reminders! Please enter the ID of the reminder you wish to modify.";

await ctx.RespondWithModalAsync(new DiscordModalBuilder().WithCustomId("reminder-modify-modal-callback").WithTitle("Modify a Reminder")
.AddTextDisplay(modalText)
.AddTextInput(new DiscordTextInputComponent("reminder-modify-id-input"), "Reminder ID")
.AddTextInput(new DiscordTextInputComponent("reminder-modify-time-input", required: false), "(Optional) Enter the new reminder time:")
.AddTextInput(new DiscordTextInputComponent("reminder-modify-text-input", required: false), "(Optional) Enter the new reminder text:"));
}
}

[Command("show")]
[Description("Show the details for a reminder.")]
[AllowedProcessors(typeof(SlashCommandProcessor))]
public static async Task ReminderShow(SlashCommandContext ctx,
[Parameter("id"), Description("The ID of the reminder to show.")] string id)
{
await ctx.DeferResponseAsync(true);

var (reminder, error) = await GetReminderAsync(id, ctx.User.Id);
if (reminder is null)
{
await ctx.RespondAsync(error, ephemeral: true);
return;
}

DiscordEmbedBuilder embed = new()
{
Title = $"Reminder `{id}`",
Description = reminder.ReminderText,
Color = new DiscordColor(0xFEC13D)
};

if (reminder.GuildId != "@me")
{
embed.AddField("Server",
$"{(await Program.discord.GetGuildAsync(Convert.ToUInt64(reminder.GuildId))).Name}");
embed.AddField("Channel", $"<#{reminder.ChannelId}>");
}

embed.AddField("Context", $"https://discord.com/channels/{reminder.GuildId}/{reminder.ChannelId}/{reminder.MessageId}");

var setTime = ((DateTimeOffset)reminder.SetTime).ToUnixTimeSeconds();

long reminderTime = ((DateTimeOffset)reminder.ReminderTime).ToUnixTimeSeconds();

embed.AddField("Set At", $"<t:{setTime}:F> (<t:{setTime}:R>)");

embed.AddField("Set For", $"<t:{reminderTime}:F> (<t:{reminderTime}:R>)");

await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().AddEmbed(embed).AsEphemeral());
}
}
}
}
17 changes: 17 additions & 0 deletions Commands/UserInfoCmds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Cliptok.Commands
{
public class UserInfoCmds
{
[Command("userinfo")]
[TextAlias("user-info", "whois")]
[Description("Show info about a user.")]
[AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))]
public async Task UserInfoSlashCommand(CommandContext ctx, [Parameter("user"), Description("The user to retrieve information about.")] DiscordUser user = null, [Parameter("public"), Description("Whether to show the output publicly.")] bool publicMessage = false)
{
if (user is null)
user = ctx.User;

await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage);
}
}
}
1 change: 1 addition & 0 deletions Constants/RegexConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ public class RegexConstants
readonly public static Regex webhook_rx = new("(?:https?:\\/\\/)?discord(?:app)?.com\\/api\\/(?:v\\d\\/)?webhooks\\/(?<id>\\d+)\\/(?<token>[A-Za-z0-9_\\-]+)", RegexOptions.ECMAScript);
readonly public static Regex id_rx = new("[0-9]{17,}");
readonly public static Regex image_url_rx = new(@"https:\/\/(?:(?:i\.)?imgur\.com\/(?:a\/)?[A-Za-z0-9]+(?:\.[A-Za-z]{3,4})?|i\.ibb\.co\/[A-Za-z0-9]+\/[A-Za-z0-9]+\.[A-Za-z]{3,4}|(?:cdn\.discordapp\.com|media\.discordapp\.net)\/attachments\/[0-9]{17,}\/[0-9]{17,}\/[A-Za-z0-9]+\.[A-Za-z]{3,4}\?ex=[A-Za-z0-9]+&is=[A-Za-z0-9]+&hm=[A-Za-z0-9]+&?(?:=&format=[A-Za-z0-9]+&width=[0-9]+&height=[0-9]+)?)");
readonly public static Regex reminder_id_rx = new("[0-9]{4}");
Comment thread
FloatingMilkshake marked this conversation as resolved.
Outdated
}
}
Loading