Wrote these YAGPDB discord bot custom commands for Harry Alisavakis' discord (Technically Speaking), but maybe others will find it useful so sharing them here ~ Enjoy!
In the server we have a channel where users can suggest themes for upcoming tech art challenges, and others can react (first emoji only) to vote for it. These commands assist with keeping track of these themes and votes.
Users use a -suggestTheme <theme>
command. Then when reactions are made (or if -updateThemes
is used) a message, posted by the bot previously during setup, is updated with an embed listing the themes, sorted by no. of votes. That message is pinned for easy access.
- Each channel can hold it's own separate themes (and embed list).
- Uses a minimum of 3 custom commands (
-setupThemes
,-suggestTheme <theme>
and-update/viewThemes
)- +1 recommended optional (
-add/edit/removeTheme <messageID>
) - +1 for "reaction" trigger to auto-update embed, (alternatively can manually use
-updateThemes
) - + some other optional/debug ones (see bottom of page)
- Some commands could likely be merged but easier to control what roles can use them this way.
- +1 recommended optional (
- Some commands will require using messageIDs. To obtain them can hold shift over a message and use the CopyID button. The result is
channelID-messageID
. Alternatively use Copy Link, the first string of numbers is the serverID, next is channelID and last is messageID
- There's no support for moving the embed to a different channel atm.
- The embed description can only display a maximum of 4096 characters (that includes the reaction name/id if not an emoji, and links which can be quite long (~85 characters per theme)). It'll likely display around 30 themes (with the most number of votes).
- The embed also has a "footer" which can display 2000 characters. This is used to list any completed themes.
- The themes are stored in a custom command database entry (UserID:
<ChannelID>
, Key:"themes"
) which has a limit of 100kb, that should be plenty of space though - I'd avoid using the system in threads, as the "Reactions" trigger command can only be configured for channels. The system does still work but you'd need to update the embed manually using
-updateThemes
I'm somewhat familiar with the go template syntax from writing templates for my website (built with gohugo) but there may be optimisations that can be done to make the below commands more efficient
- https://docs.yagpdb.xyz/commands/custom-commands
- https://docs.yagpdb.xyz/reference/templates
- https://docs.yagpdb.xyz/reference/templates/functions
- https://learn.yagpdb.xyz/
- Use this before all other commands, to enable theme suggestions for that channel. It posts the embed list and initalises saved data.
- Usage :
-setupThemes
$canOverride
controls whether-setupThemes
can override saved data or not. Ideally keep this set to false. If you need to override can temporarily set this to true.
{{- $canOverride := false }}
{{/* --- */}}
{{- $allowSetupInThreads := eq (lower .StrippedMsg) "thread" }}
{{ if and .Channel.IsThread (not $allowSetupInThreads) }}
{{.Message.Author.Mention }} Themes setup is not recommened in threads, as it's not possible to configure YAGPDB to use reactions triggers for threads.
(Technically it can still function but users would need to use `-updateThemes` / `-viewThemes`. If this is okay, use `-setupThemes thread`)
{{- else }}
{{- $hasThemes := false }}
{{- $channelID := .Channel.ID }}
{{- if (dbGet $channelID "themes") }}
{{- $hasThemes = true}}
{{- end }}
{{- if or (not $hasThemes) (and $hasThemes $canOverride) }}
{{- $messageID := sendMessageRetID nil (complexMessage
"content" "Hello, Themes!"
"embed" (cembed
"title" "Themes"
"description" ""
"color" 655246 ))}}
{{- dbSet $channelID "themesEmbed" (cslice $messageID 0) }}
{{- dbSet $channelID "themes" (sdict) }}
{{- deleteTrigger 0 }}
{{- else }}
{{.Message.Author.Mention }} There is already themes data for this channel.
(If you want to force the data to reset, edit the command to `$canOverride := true`)
{{- end }}
{{- end }}
- Used to suggest a theme. Only the first line of the message is used.
- Usage :
-suggestTheme <theme>
- Default character limit of 30. Can change
$characterLimit
if you want to allow longer suggestions, but recommend not to set it too large due to discord limits. (Message must be under 2000 characters. Embed description can show up to 4096 characters)
{{- $characterLimit := 30 }}
{{/* --- */}}
{{- $args := parseArgs 1 "" (carg "string" "theme name") }}
{{- $theme := trimSpace (index (split ($args.Get 0) "\n") 0) }}
{{- $serverID := .Guild.ID }} {{- $channelID := .Channel.ID }} {{- $msgID := .Message.ID }}
{{- $db := (dbGet $channelID "themes") }}
{{- if $db }}
{{- $data := $db.Value }}
{{- if le (len $theme) $characterLimit }}
{{- /*Themes data is stored in CCDB id=channelID, key="themes", value=dict(messageIDs, slice(name, voteCount, emoji, date:"yyyy-mm-dd", isCompleted))*/}}
{{- /*Check if already exists*/}}
{{- $exists := false }} {{- $existsID := 0 }} {{- $themeLower := lower $theme }}
{{- range $messageID, $themeSlice := $data }}
{{- if and (not $exists) $themeSlice }}
{{- $exists = eq $themeLower (lower (index $themeSlice 0)) }}
{{- $existsID = $messageID }}
{{- end }}
{{- end }}
{{- if $exists }}
{{.Message.Author.Mention }} That theme has already been suggested by someone else! :)
( https://discord.com/channels/{{ $serverID }}/{{ $channelID }}/{{ $existsID }} )
*(This message, and your `-suggestTheme` message, will be automatically removed in 10 secs)*
{{- deleteTrigger 10 }} {{- deleteResponse 10 }}
{{- else }}
{{- /*Add to Data & Save*/}}
{{- $date := formatTime .Message.Timestamp.Parse "2006-01-02" }}
{{- $data.Set (str $msgID) (cslice $theme 0 "" $date false) }}
{{- dbSet $channelID "themes" $data }}
{{.Message.Author.Mention }} Thank you for suggesting the theme `{{$theme}}`! ✨
You may want to add an appropriate reaction emoji to your message.
Your theme may be included in future voting rounds if users react to that same emoji!
*(This message will be automatically removed in 30 secs)*
{{- deleteResponse 30 }}
{{- end }}
{{- else }}
{{.Message.Author.Mention }} Sorry, theme suggestions must be {{$characterLimit}} characters or less. Please try again!
Only the first line is used so if your theme requires extra info (e.g. clarifications), you can put them on a separate line (Shift+Enter), or use a separate message.
*(This message, and your `-suggestTheme` message, will be automatically removed in 30 secs)*
{{- deleteTrigger 30 }} {{- deleteResponse 30 }}
{{- end }}
{{- else }}
{{.Message.Author.Mention }} Theme suggestions is not setup in this channel.
*(This message, and your `-suggestTheme` message, will be automatically removed in 10 secs)*
{{- deleteTrigger 10 }} {{- deleteResponse 10 }}
{{- end }}
- This handles 3 commands in one :
-updateThemes
: Updates the embed list. Can be used with optional string to change the message before the embed.-viewThemes
: Updates the embed list and also sends a message containing it mentioning the user. That message will be removed when the command (-viewThemes
or-viewThemesFile
) is used again.-viewThemesFile
: Sends the theme data in a message file attachment. That message will be removed when the command (-viewThemes
or-viewThemesFile
) is used again.
{{/* --- */}}
{{- $serverID := .Guild.ID }} {{- $channelID := .Channel.ID }}
{{- $db := dbGet $channelID "themesEmbed" }} {{- /* stored as slice(mainMessageID, tempMessageID) */}}
{{- if $db }}
{{- /*Themes data is stored in CCDB id=channelID, key="themes", value=dict(messageIDs, slice(name, voteCount, emoji, date:"yyyy-mm-dd", isCompleted))*/}}
{{ $data := (dbGet $channelID "themes").Value }}
{{- if not .StackDepth }}
{{- addReactions "loading2:480811019611144216" }}
{{- end }}
{{- if .Channel.IsThread }} {{- /* Workaround for loading votes in threads (much slower). Channels must use the reaction trigger command instead */}}
{{- range $messageID, $themeSlice := $data }}
{{- if $themeSlice }}
{{- if not (eq (index $themeSlice 4) true) }}
{{ $msg := getMessage nil $messageID }}
{{ if $msg }}
{{- $reactions := $msg.Reactions }} {{- $emoji := "" }} {{- $votes := 0 }}
{{- if $reactions }}
{{- $reaction := index $reactions 0 }} {{- $emoji = $reaction.Emoji.MessageFormat }} {{- $votes = $reaction.Count }}
{{- end }}
{{- $themeSlice.Set 1 $votes }} {{- $themeSlice.Set 2 $emoji }}
{{ end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- /* Sort themes by no. of votes */}}
{{- $themes := cslice }} {{- $completed := cslice }}
{{- range $messageID, $themeSlice := $data }}
{{- if $themeSlice }}
{{- if eq (index $themeSlice 4) true }}
{{- $completed = $completed.Append $messageID }}
{{- else }}
{{- $voteCount := (index $themeSlice 1) }}
{{- $themes = $themes.Append (cslice $voteCount $messageID) }}
{{- end }}
{{- end}}
{{- end }}
{{- /* The following is based on : https://yagpdb-cc.github.io/code-snippets/selection-sort */}}
{{- /* May error for large arrays/slices? :( */}}
{{- $len := len $themes }}
{{- range seq 0 $len }}
{{- $min := . }}
{{- range seq (add . 1) $len }}
{{- if lt (index $themes $min 0) (index $themes . 0) }} {{- $min = . }} {{- end }}
{{- end }}
{{- if ne $min . }}
{{- $ := index $themes . }}
{{- $themes.Set . (index $themes $min) }}
{{- $themes.Set $min $ }}
{{- end }}
{{- end }}
{{- /* Handle embed */}}
{{- $embedContents := cslice }} {{- $embedContentLength := 0 }}
{{- range $themes }} {{- /* (voteCount, messageID) */}}
{{- $messageID := (index . 1) }} {{- $themeSlice := $data.Get (str $messageID) }}
{{ with $themeSlice }} {{- /* (name, voteCount, emoji, date:"yyyy-mm-dd", isCompleted) */}}
{{- $link := (joinStr "" "https://discord.com/channels/" $serverID "/" $channelID "/" $messageID) }}
{{- $str := (joinStr "" "[" (index . 2) "] x " (index . 1) " - [" (index . 0) "](" $link ")" ) }}
{{- $embedContentLength = add $embedContentLength (add (len $str) 1) }}
{{- if lt $embedContentLength 4000 }}
{{- $embedContents = $embedContents.Append $str }}
{{- end }}
{{- end }}
{{- end }}
{{- $embedFooter := "" }} {{- $embedContentLength = 0 }}
{{- if gt (len $completed) 0 }}
{{- $embedFooter = "Completed :" }}
{{- range $index, $messageID := $completed }}
{{- $themeSlice := $data.Get $messageID }}
{{- $str := (index $themeSlice 0) }}
{{- $embedContentLength = add $embedContentLength (add (len $str) 2) }}
{{- if lt $embedContentLength 2000 }}
{{- if eq $index 0 }}
{{- $embedFooter = joinStr " " $embedFooter $str }}
{{- else }}
{{- $embedFooter = joinStr ", " $embedFooter $str }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- $embed := cembed
"title" "Themes"
"description" (joinStr "\n" $embedContents.StringSlice)
"color" 655246
"footer" (sdict "text" $embedFooter)
}}
{{- /* Update embed messages */}}
{{- $embedIDs := $db.Value }} {{- $mainEmbedID := (index $embedIDs 0) }} {{- $tempEmbedID := (index $embedIDs 1) }}
{{- if not (getMessage nil $mainEmbedID) }}
It seems the main embedded themes message has been deleted, reposting...
(When it appears you may want to pin it to keep track of it!)
{{- deleteResponse 10 }}
{{- $mainEmbedID = sendMessageRetID nil "Hello, Themes!" }}
{{- $embedIDs.Set 0 $mainEmbedID }}
{{- /* Save themesEmbed */}}
{{- dbSet $channelID "themesEmbed" $embedIDs }}
{{- end }}
{{- $content := (lower .Message.Content) }}
{{- if hasPrefix $content "-viewthemes" }}
{{- /* Delete previous temp embed message and send new one */}}
{{- deleteMessage nil $tempEmbedID 1 }}
{{- if hasSuffix $content "file" }}
{{- $lines := cslice }}
{{- range $messageID, $themeSlice := $data }}
{{- $v := "" }}
{{- range $themeSlice }}
{{- $v = joinStr "|" $v (print .) }}
{{- end }}
{{- $lines = $lines.Append (joinStr "|" $messageID $v) }}
{{- end }}
{{- $tempEmbedID = sendMessageRetID nil (complexMessage
"content" (joinStr "" .Message.Author.Mention " Here's a file containing the current theme data!")
"file" (joinStr "\n" $lines.StringSlice) )}}
{{- else }}
{{- $tempEmbedID = sendMessageRetID nil (complexMessage
"content" (joinStr "" .Message.Author.Mention " Here's the themes embed!")
"embed" $embed )}}
{{- end }}
{{- /* Save themesEmbed */}}
{{- $embedIDs.Set 1 $tempEmbedID }}
{{- dbSet $channelID "themesEmbed" $embedIDs }}
{{- editMessage nil $mainEmbedID $embed }}
{{- else }}
{{- $args := parseArgs 0 "" (carg "string" "content") }}
{{- $mainContent := ($args.Get 0) }}
{{- if $mainContent }}
{{- editMessage nil $mainEmbedID (complexMessageEdit "content" $mainContent "embed" $embed) }}
{{- else }}
{{- editMessage nil $mainEmbedID $embed }}
{{- end }}
{{ $tempMsg := getMessage nil $tempEmbedID }}
{{- if $tempMsg }}
{{- if eq (len $tempMsg.Attachments) 0 }}
{{- editMessage nil $tempEmbedID $embed }}
{{- end }}
{{- end }}
{{- end }}
{{- else }}
{{.Message.Author.Mention }} Themes is not setup in this channel.
*(This message will be automatically removed in 10 secs)*
{{- deleteResponse 10 }}
{{- end }}
{{- if not .StackDepth }}
{{- deleteTrigger 1 }}
{{- end }}
- Used for channels to handle counting of votes. Will also trigger updating the embed.
$updateCCID
here needs the CCID of the above-updateThemes
command (it's the number of the command without the#
)
{{- $updateCCID := 0 }}
{{/* --- */}}
{{- $serverID := .Guild.ID }} {{- $channelID := .Channel.ID }} {{- $msgID := .Reaction.MessageID }}
{{- $db := (dbGet $channelID "themes") }}
{{- if $db }}
{{- /* Update Themes Data, stored as dict(messageIDs, slice(name, voteCount, emoji, date:"yyyy-mm-dd", isCompleted)) */}}
{{- $data := $db.Value }} {{- $themeSlice := $data.Get (str $msgID) }}
{{- if $themeSlice }}
{{- /* Get Emoji & Votes */}}
{{- $reactions := .ReactionMessage.Reactions }} {{- $emoji := "" }} {{- $votes := 0 }}
{{- if $reactions }}
{{- $reaction := index $reactions 0 }}
{{- $emoji = $reaction.Emoji.MessageFormat }} {{- $votes = $reaction.Count }}
{{- end }}
{{- $themeSlice.Set 1 $votes }} {{- $themeSlice.Set 2 $emoji }}
{{- /* unsure if I need to set the slice back on the dict, I think it's like a reference type so shouldn't need to */}}
{{- dbSet $channelID "themes" $data }}
{{- /* Trigger Embed to Update */}}
{{- if $updateCCID }} {{- execCC $updateCCID nil 0 0 }} {{- end }}
{{- end }}
{{- end }}
This handles 3 commands in one :
-addTheme <messageID>
: Adds an existing message to the themes embed. The first line of the message becomes the theme name (text displayed on embed).- Can also use this with
<messageID>|name:<string>
arguments instead, to change the name while adding, see-editTheme
below for info. - Warning : Unlike
-suggestTheme
this doesn't check if theme already exists and has no character limit
- Can also use this with
-removeTheme <messageID>
: Removes the theme with the given messageID-editTheme <messageID>|<key:value>
: Edit a value associated with the theme.- Valid keys are :
name
(the string that was used with-suggestText
)complete
(completed themes are listed after main list, on embed "footer")- (Can also have multiple key:value pairs, separated by
|
. Unrecognised keys will be ignored)
- Examples :
-editTheme 950384025418215444|name:New Text
-editTheme 950384025418215444|complete
(defaults to true)-editTheme 950384025418215444|name:New Text|complete:false
- Valid keys are :
- All of these also support adding/removing/editing multiple themes. Put each
<messageID>
(or<messageID>|<key:value>
) on a new line.
{{/* --- */}}
{{- $channelID := .Channel.ID }}
{{- $args := parseArgs 1 "" (carg "string" "messageID") }}
{{- $db := dbGet $channelID "themes" }}
{{- if $db }}
{{- /*Themes data is stored as dict(messageIDs, slice(name, voteCount, emoji, date:"yyyy-mm-dd", isCompleted))*/}}
{{- $data := $db.Value }} {{- $lines := split ($args.Get 0) "\n" }} {{- $entries := cslice }} {{- $lowerContent := (lower .Message.Content) }}
{{- range $lines }}
{{- $entries = $entries.Append (split . "|") }}
{{- end }}
{{- if hasPrefix $lowerContent "-add" }}
{{- range $entries }}
{{- $msgID := toInt64 (trimSpace (index . 0)) }}
{{- $msg := getMessage nil $msgID }}
{{- if $msg }}
{{- $content := reReplace `^(?i)-suggesttheme\s` $msg.Content "" }}
{{- $theme := trimSpace (index (split $content "\n") 0) }} {{- $date := formatTime $msg.Timestamp.Parse "2006-01-02" }}
{{- $reactions := $msg.Reactions }} {{- $emoji := "" }} {{- $votes := 0 }}
{{- if $reactions }}
{{- $reaction := index $reactions 0 }} {{- $emoji = $reaction.Emoji.MessageFormat }} {{- $votes = $reaction.Count }}
{{- end }}
{{- $data.Set (str $msgID) (cslice $theme $votes $emoji $date false) }}
{{- end }}
{{- end }}
{{- end}}
{{- if hasPrefix $lowerContent "-remove" }}
{{- range $entries }}
{{- $msgID := toInt64 (trimSpace (index . 0)) }} {{- $data.Del (str $msgID) }}
{{- end }}
{{- else }} {{- /* add & edit */}}
{{- range $entries }}
{{- $msgID := toInt64 (trimSpace (index . 0)) }} {{- $themeSlice := $data.Get (str $msgID) }} {{- $edits := . }}
{{- /* handle key:value pairs */}}
{{- range seq 1 (len .) }}
{{- $edit := split (index $edits .) ":" }} {{- $len := len $edit }}
{{- if eq $len 1 }}
{{- $key := index $edit 0 }}
{{- if eq $key "complete" }} {{- $themeSlice.Set 4 true }} {{- end }}
{{- else if ge $len 2 }}
{{- $key := index $edit 0 }} {{- $value := index $edit 1 }}
{{- if eq $key "name" }} {{- $themeSlice.Set 0 $value }} {{- end }}
{{- if eq $key "complete" }} {{- $themeSlice.Set 4 (eq (lower $value) "true") }} {{- end }}
{{- end }}
{{- end }}
{{- /* $data.Set (str $msgID) $themeSlice */}}
{{- end }}
{{- end }}
{{- dbSet $channelID "themes" $data }}
{{- else }}
{{.Message.Author.Mention }} Themes is not setup in this channel.
{{- end }}
{{- deleteTrigger 5 }} {{- deleteResponse 5 }}
- Selects a number of entries from themes channel, randomly but weighted by vote/reaction count
- Usage :
-randomThemes <themesChannelID> <numberOfThemes>
- Command specifies channelID so it can be typed in other channels (e.g. mods-only)
{{- $args := parseArgs 2 "" (carg "string" "themes channelID") (carg "int" "number of entires") }}
{{- $db := dbGet (toInt64 (trimSpace ($args.Get 0))) "themes" }}
{{- if $db }}
{{- /*Themes data is stored in CCDB id=channelID, key="themes", value=dict(messageIDs, slice(name, voteCount, emoji, date:"yyyy-mm-dd", isCompleted))*/}}
{{- $data := $db.Value }}
{{- range seq 0 ($args.Get 1) }}
{{- $weightSum := 0 }}
{{- range $messageID, $themeSlice := $data }}
{{- if $themeSlice }}
{{- if (eq (index $themeSlice 4) true) }} {{- continue }} {{- end }}
{{- $votes := (index $themeSlice 1) }}
{{- $weightSum = (add $weightSum $votes) }}
{{- end }}
{{- end }}
{{- if eq $weightSum 0 }}
(Ran out of entries)
{{- break }}
{{- end }}
{{- $rand := randInt $weightSum }}
{{- $result := "" }}
{{- range $messageID, $themeSlice := $data }}
{{- if $themeSlice }}
{{- if (eq (index $themeSlice 4) true) }} {{- continue }} {{- end }}
{{- $votes := (index $themeSlice 1) }}
{{- if lt $rand $votes }}
{{- $result = joinStr " " (index $themeSlice 2) "|" (index $themeSlice 0) }}
{{- $data.Del $messageID }}
{{- break }}
{{- end }}
{{- $rand = sub $rand $votes }}
{{- end }}
{{- end }}
{{ $result }}
{{- end }}
{{- else }}
{{.Message.Author.Mention }} That channelID does not contain a themes system!
*(This message will be automatically removed in 10 secs)*
{{- deleteResponse 10 }}
{{- end }}
- Makes the bot add to the existing reaction : (useful as I was testing alone and wanted to make sure sorting was working, but note that this won't trigger the "reaction" command. You should toggle a reaction on the same message after)
{{$args := parseArgs 1 ""
(carg "string" "messageID")
}}
{{$msgID := trimSpace ($args.Get 0)}}
{{$msg := getMessage nil $msgID}}
{{$reaction := (index $msg.Reactions 0).Emoji.APIName}}
{{addMessageReactions nil $msgID $reaction}}
{{deleteTrigger 0}}
- Delete Database Entries : (useful if you no longer want themes saved to the channel the command is used in. Make sure you set this to mod-only. Would recommend turning off the trigger once used, to avoid accidental use later on!)
{{ $channelID := .Channel.ID }}
{{ dbDel $channelID "themes" }}
{{ dbDel $channelID "themesEmbed" }}