Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
164 lines
6.7 KiB
Go
164 lines
6.7 KiB
Go
package help
|
|
|
|
import (
|
|
"log"
|
|
"unicode/utf8"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
)
|
|
|
|
var Help = &discordgo.ApplicationCommand{
|
|
Name: "help",
|
|
Description: "Get help with the bot",
|
|
}
|
|
|
|
// A message's embeds share a limit of 6000 characters, and one embed holds at
|
|
// most 25 fields of 1024 characters. The reference is longer than that, so it
|
|
// is sent as a few messages.
|
|
const (
|
|
maxEmbedChars = 5000
|
|
maxFieldChars = 1024
|
|
)
|
|
|
|
// docsURL is where each command is explained in full, with its options and
|
|
// who may use it.
|
|
const docsURL = "https://velox.fernandovideira.com/docs/commands"
|
|
|
|
// col is one short column of the help: a list of commands, no descriptions.
|
|
// Fields are inline, so Discord lays three of them side by side and the help
|
|
// is wide and short instead of narrow and long.
|
|
func col(name string, lines ...string) *discordgo.MessageEmbedField {
|
|
v := ""
|
|
for i, l := range lines {
|
|
if i > 0 {
|
|
v += "\n"
|
|
}
|
|
v += l
|
|
}
|
|
return &discordgo.MessageEmbedField{Name: name, Value: v, Inline: true}
|
|
}
|
|
|
|
func fields() []*discordgo.MessageEmbedField {
|
|
return []*discordgo.MessageEmbedField{
|
|
col("Fun",
|
|
"`/ping` `/joke` `/coinflip`", "`/dice` `/rps` `/rpsstats`", "`/rpsleaderboard`"),
|
|
col("Music (DJ role)",
|
|
"`/play` `/queue` `/volume`", "`/music skip` `pause` `resume`", "`/music stop` `repeat`", "`/music remove` `move`", "`/music toggle` (Manage Server)"),
|
|
col("Sessions and time",
|
|
"`/schedule create`", "`/schedule list`", "`/schedule reschedule`", "`/timezone set` `show`"),
|
|
col("Tickets",
|
|
"Open one from the panel.", "`/ticket add` `close`", "`/ticket claim` `remove` (staff)", "`/tickets setup` `settings`", "`/tickets category` `panel` `list`"),
|
|
col("Security",
|
|
"`/security status` `settings` `history`", "`/security toggle`", "`/security joingate` `words` `raid`", "`/security antinuke` `heat` `alert-dms`", "`/security trust` `release` `end-raid`", "`/security alerts` `quarantine-role`", "`/security snapshot` `restore`", "`/lockdown` `/unlock`"),
|
|
col("Moderation",
|
|
"`/moderation purge`", "`/moderation timeout` `untimeout`", "`/moderation kick` `ban` `unban`", "`/moderation slowmode`", "`/moderation bans` `timeouts`"),
|
|
col("Meeting rooms",
|
|
"`/meeting add-lobby`", "`/meeting remove-lobby` `lobbies`", "`/meeting lock` `lock-private`", "`/meeting unlock`", "`/meeting invite` `uninvite`"),
|
|
col("Leveling",
|
|
"`/rank` `/leaderboard` `/rewards`", "`/slvl toggle` `status`", "`/slvl set` `set-channel`", "`/slvl set-message`", "`/slvl set-reward` `remove-reward`"),
|
|
col("Projects",
|
|
"`/projects list`", "`/projects create`", "`/projects add-helper`", "`/projects close`"),
|
|
col("Welcome and roles",
|
|
"`/config setwelcomechannel`", "`/config setwelcomemessage`", "`/config setwelcomedm`", "`/config setwelcomegif`", "`/config setdefaultrole`"),
|
|
col("Logging and Twitch",
|
|
"`/config setlogchannel`", "`/config enablelogging` `disablelogging`", "`/config addstreamer` `removestreamer`", "`/config streamers`", "`/config toggletwitch`", "`/config settwitchnotificationchannel`"),
|
|
col("Settings and beta",
|
|
"`/config view` `clear`", "`/config beta-features` (server owner)", "Bot owner, in a DM with me:", "`/beta set` `off` `status`"),
|
|
{
|
|
Name: "Everything in detail",
|
|
Value: "Options, permissions and what each command does: " + docsURL + "\nMost setup commands need **Manage Server**. `/help` shows this list again.",
|
|
},
|
|
}
|
|
}
|
|
|
|
func fieldSize(f *discordgo.MessageEmbedField) int {
|
|
return utf8.RuneCountInString(f.Name) + utf8.RuneCountInString(f.Value)
|
|
}
|
|
|
|
// embeds splits the reference into embeds that fit Discord's limits.
|
|
func embeds(author *discordgo.MessageEmbedAuthor) []*discordgo.MessageEmbed {
|
|
var out []*discordgo.MessageEmbed
|
|
cur := &discordgo.MessageEmbed{
|
|
Title: "Velox Bot Help",
|
|
Description: "Every command, by area. Some need server permissions or a role.",
|
|
Color: 0xFFA500,
|
|
Author: author,
|
|
}
|
|
size := utf8.RuneCountInString(cur.Title) + utf8.RuneCountInString(cur.Description) + utf8.RuneCountInString(author.Name)
|
|
for _, f := range fields() {
|
|
if size+fieldSize(f) > maxEmbedChars || len(cur.Fields) >= 25 {
|
|
out = append(out, cur)
|
|
cur = &discordgo.MessageEmbed{Title: "Velox Bot Help (continued)", Color: 0xFFA500}
|
|
size = utf8.RuneCountInString(cur.Title)
|
|
}
|
|
cur.Fields = append(cur.Fields, f)
|
|
size += fieldSize(f)
|
|
}
|
|
return append(out, cur)
|
|
}
|
|
|
|
// HelpHandler sends the help by direct message. If that isn't possible (the
|
|
// member has DMs off, or shares no server with the bot), it shows the help to
|
|
// them in the channel instead, as a message only they can see.
|
|
func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
author := discordgo.MessageEmbedAuthor{
|
|
Name: "Velox Bot",
|
|
URL: "https://gitea.fernandovideira.com/FernandoJVideira/velox-bot",
|
|
}
|
|
list := embeds(&author)
|
|
|
|
// Used in a DM with the bot, there is nowhere else to send it.
|
|
if i.GuildID == "" {
|
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{Embeds: list},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Answer first (sending the DM can take a moment), and edit the answer
|
|
// once we know where the help went.
|
|
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{Flags: discordgo.MessageFlagsEphemeral},
|
|
}); err != nil {
|
|
log.Printf("help: cannot answer: %v", err)
|
|
return
|
|
}
|
|
|
|
userID := ""
|
|
if i.Member != nil && i.Member.User != nil {
|
|
userID = i.Member.User.ID
|
|
} else if i.User != nil {
|
|
userID = i.User.ID
|
|
}
|
|
|
|
if err := sendByDM(s, userID, list); err != nil {
|
|
// Typically "Cannot send messages to this user": DMs are off.
|
|
// The context says where the command came from, which is what to look at
|
|
// when "no mutual guilds" appears for someone who is in a server with
|
|
// the bot: a command run through a user install, or in a server the bot
|
|
// isn't actually a member of, has no shared server for Discord to see.
|
|
log.Printf("help: DM not possible, showing it here (guild %q, context %d, installs %v): %v",
|
|
i.GuildID, i.Context, i.AuthorizingIntegrationOwners, err)
|
|
note := "I couldn't DM you (your DMs may be off), so here it is."
|
|
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{Content: ¬e, Embeds: &list})
|
|
return
|
|
}
|
|
done := "Check your DMs for help."
|
|
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{Content: &done})
|
|
}
|
|
|
|
func sendByDM(s *discordgo.Session, userID string, list []*discordgo.MessageEmbed) error {
|
|
dm, err := s.UserChannelCreate(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, e := range list {
|
|
if _, err := s.ChannelMessageSendEmbed(dm.ID, e); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|