Skip to content

Instantly share code, notes, and snippets.

@mesmere
Last active July 7, 2023 12:35
Show Gist options
  • Select an option

  • Save mesmere/3ad599c3f6083fd933860bdde1efb348 to your computer and use it in GitHub Desktop.

Select an option

Save mesmere/3ad599c3f6083fd933860bdde1efb348 to your computer and use it in GitHub Desktop.

This is a suite of custom commands for vetting people into a discord server using yagpdb's ticket system.

Relevant roles

  • [no role] - Can only see #start-here and #rules.
  • admin - Can do everything.
  • approver - Can approve or deny vetting tickets but shouldn't have other permissions.
  • anti-liberal aktion - Can see vetting and talk to applicants but can't approve.
  • vetted - Applicants who have been approved. Gives them access to #roles and #general
  • liberal in the walls - Additional role for applicants who have been approved but need some restrictions added (we add a role icon to tell people they're a lib)
  • chattanoogan - Selectable in #roles to get access to the rest of the server. We make people take this as a self-role in the hope that while they're there they'll select a pronoun role too.

Relevant channels

  • #start-here - Contains a single welcome message with a reaction that triggers opening a new vetting ticket. Not visible to vetted.
  • #vetting-logs - Has a log message for each person in vetting. When their vetting status changes, the log message is edited to show their current status. Also, when a vetting ticket is closed a transcript of the ticket channel is dumped and uploaded here as an attachment. This channel is only visible to admins/approvers/antiliberalaktion.
  • #[num]-[username]-vetting - This is a different channel for every user so that only the applicant and admins/approvers/antiliberalaktion can see the channel. This is the vetting ticket itself. (Username is sanitized and truncated to 20 characters.)

The vetting process (simple)

  1. Click the react in #start-here to open a new channel.
  2. The bot sends the vetting questions in the channel. It also sends a new log message in #vetting-logs, which will be updated with the vetting ticket status.
  3. The applicant has 3 days to answer the questions to approvers' satisfaction. (After 3 days with no messages from the applicant, the ticket auto-closes.)
  4. Approvers can react β˜‘οΈ to any message in the ticket, or to the log message, in order to approve the applicant. Alternatively they can react with πŸ‘οΈ, which approves while additionally giving the "liberal in the walls" role.
  5. Approvers can react ⏸️ to the log message in order to block any other approver from approving. This doesn't work in the channel itself because we don't want the applicant to see that they've been put on hold.
  6. Approvers can react πŸ‘’ to any message in the ticket, or to the log message, in order to kick the applicant.
  7. Approvers can react ❌ to any message in the ticket, or to the log message, in order to ban the applicant for 6 months. This is the typical way we deny now that vetting activity has picked up to the point of being a serious load on approvers.

vetting-logs screenshot

Database schema

  • User 0, key "vet-user-%" where % is the int64 user ID of the applicant
  • User 0, key "vet-chan-%" where % is the int64 channel ID of the ticket
  • User 0, key "vet-log-%" where % is the int64 message ID of the log message in #vetting-logs

In all cases the db entries are stored under the "user" 0 but this has nothing to do with users, it's just a database namespacing thing.

The same map is stored under all three keys:

(sdict 
  "UserID" $userID 
  "ChannelID" $channelID 
  "LogMessageID" $logMessageID
  "QsMessageID" $qsMessageID
)

This use of the database means that when we're given any one piece of info by the trigger context for an event, we can obtain everything that we need. For example, if the applicant reacts to the #start-here message, we only know their user ID but we can query "vet-user-%" to find out if there's already an open vetting ticket for them. Or if someone approves inside the vetting channel then we only know the channel ID, but we can query "vet-chan-%" to find out the log message ID in order to update the ticket status.

QsMessageID is the message ID of the vetting questions message inside their ticket. This is necessary so that we can update the expiration time inside the ticket every time the applicant sends a message there.

A note on other tickets

We still want to let people open normal tickets to communicate with admins about server issues. In this case we don't want approvers to be able to see those ticket channels. Fortunately yag lets us do this. Just set the "moderator" option to the approver role, and then set all new tickets to admin only ("ao") mode unless they're vetting tickets. The tickets ao command removes mod permissions from the channel but leaves other users.

screenshot

The vetting process (in detail)

Starting

An applicant (non-admin non-approver non-antiliberalaktion) clicking the reaction in #start-here begins the process. We remove the reaction so that nobody can see how many have been vetted. We check "vet-user-%" to make sure the user doesn't already have an open vetting ticket.

If they don't already have an open vetting ticket, we create a new one. Their name goes through a normalization step to determine the channel name: first romanize it with the sanitizeText function, then pick out all of the alphanumeric characters, then truncate it to 20 characters. It turns out that Discord channel names have a lot more restrictions on them than user names, and this is about the best we can do to try to make the vetting channel names identifiable. Be aware though that this is incompatible with server discovery because it checks for NSFW channel names, and anyone with a NSFW name can just open a vetting ticket and delist you...

Once the vetting channel is open, we send the vetting questions inside the new channel, send a log message in #vetting-logs, populate the three database entries, and schedule the timeout execCC for 3 days.

The database entries are set to expire slightly later than the proper timeout handler so that they don't take up limit space forever if admins are meddling (e.g. someone closes the ticket manually).

Ticket activity

An applicant (non-admin non-approver non-antiliberalaktion) sending a message inside a vetting channel should reset the 3-day inactivity timeout. Because there's no fixed set of vetting channels, we have to trigger on every such message sent in the whole server. To ease load on yag we first check if the channel name ends in "-vetting" and then (since users can actually create arbitrarily-named channels with the ticket system) we query "vet-chan-%" to make sure it's a valid vetting channel.

Once we have the ticket info we need from the database we just reinsert the database entries (so that they don't expire), reschedule the timeout execCC, and update the "ticket expires at" footer in the vetting questions message.

Pausing a ticket

If an approver sees red flags they can add a ⏸️ react to the log message in #vetting-logs. This is mainly a holdover from the old vetting system but it's still occasionally useful. With a ⏸️ reaction in place, nobody can approve that ticket. You can then discuss in approver chat without worrying about them getting through.

Denying a ticket

An approver can deny with ❌ to ban or πŸ‘’ to kick.

Kick DM Ban DM
kick DM ban DM

Approving a ticket

Ticket approval can happen either with the β˜‘οΈ or πŸ‘οΈ reacts. They do the same thing except for the additional "liberal in the walls" role granted by πŸ‘οΈ.

Approval updates the log message, closes the ticket, unschedules the timeout cc, deletes the database entries, grants the vetted/liberal role(s), and sends a welcome message in #general which tells the new member to set their self-roles.

These approve/deny reactions can be given by an approver in two different ways: either inside the ticket (on any message) or to the log message in #vetting-logs. Because there's no fixed set of vetting channels we have to trigger on every approver/admin reaction in the entire server and then check the channel name to see if it looks vetting-related. Then we look up the ticket info sdict that we need by checking "vet-chan-%" (where % is the reaction's channel ID) and "vet-log-%" (where % is the reaction's message ID) - this handles the two cases.

Maintenance denial

Tickets can also be closed by timeout (currently 3 days) or by an admin/approver executing -close in the ticket channel. This latter option is better than using /tickets close because it updates the #vetting-log message and cleans up the database. If you manually close an empty ticket then they won't be able to create a new one until the database entries expire on their own.

Early pruning

A CC runs on an interval to make sure that everyone with an open vetting ticket is still in the server. A ticket will be closed when its owner is no longer around. This is difficult though, because ticket close must be performed with execCC to change the channel context to the correct channel first, but on free tier we're limited to only a single invocation of execCC every time the scheduled task triggers! Currently it just attempts to close everything it needs to close, and on free tier it will fail after the first one. It just works its way through one closed ticket at a time.

If someone manually calls -ticket close then the pruning task won't be able to close the ticket again so it handles this case specially to make sure that the database entries still get cleaned up.

{{/* trigger on admin -populate in #start-here */}}
{{ sendMessage nil "https://files.catbox.moe/vmq7u8.jpg" }}
{{$id := sendMessageRetID nil (cembed "description" "Like many other leftist Discord servers, we use vetting questions to keep our community healthy. Until you get vetted you won't be able to talk in the server.\n\nPlease **click the reaction below this message** to open a new vetting ticket. Everyone inside is wishing you luck! <:fidel_salute_big:974062102924918804>") }}
{{ addMessageReactions nil $id "heart_sickle:973994307113590804" }}
{{/* trigger on added react in #start-here */}}
{{ $vettingMessage := `Please answer the questions below. You can edit your answers or add additional information at any time, but unapproved tickets will autoclose after 3 days of inactivity. No need to ping approvers; we'll review your answers when we get a chance.
*If you're having a hard time understanding the questions, let us know.*` }}
{{ $vettingQuestions := `**1. Do you think that it's possible/realistic to do better than capitalism?**
_If yes_, what kind of system do you think would be better, and what would be necessary to get there?
**2. What is the relevance of imperialism to the struggle for a better world?**
Whom does imperialism affect and how does it affect them (benefits/drawbacks)?
**3. What should be our position with respect to colonized nations?**
Palestinians, indigenous peoples of the Americas and Oceania, Irish, and others have land claims which conflict with those of people currently occupying the land. Please describe how would you like to see those conflicts addressed going forward.
**4. What's your view on LGBT liberation, racial justice, and similar movements?**
Please explain how you see these movements in relation to your answer to question 1.
**5. How would you describe your political tendency?**
Examples: anarchist, Marxist-Leninist, democratic socialist, etc. This is a non-sectarian space!
**6. How did you find our server?**
If somebody else invited you, please mention their full username (e.g. exampleuser#1917).
**7. Did you carefully read _all_ of the #rules?**
This question is why most people's answers are rejected. We encourage you to read _all_ of the rules before answering 'yes,' in case you missed something. <:comfy:997639943188906086>` }}
{{ deleteMessageReaction nil .ReactionMessage.ID .User.ID .Reaction.Emoji.APIName }}
{{ if not (dbGet 0 (print "vet-user-" .User.ID)) }}
{{ $sanitizedName := or (joinStr "-" (reFindAll "[a-zA-Z0-9]+" (sanitizeText .User.Username))) "unknown" }}
{{ $chanName := print (slice $sanitizedName 0 (toInt (min (len $sanitizedName) 20))) "-vetting" }}
{{ with createTicket nil $chanName }}
{{ $logMessageID := sendMessageNoEscapeRetID "vetting-logs" (cembed "title" (print $.User.Mention " vetting") "description" (print "<#" .ChannelID "> has been opened.\nβ€£ React β˜‘οΈ **here** or **inside the ticket** to approve.\nβ€£ React πŸ‘οΈ **here** or **inside the ticket** to approve on probation.\nβ€£ React πŸ‘’ **here** or **inside the ticket** to kick.\nβ€£ React ❌ **here** or **inside the ticket** to deny/ban.\nβ€£ React ⏸️ **here** to block approval.")) }}
{{ sendMessageNoEscape .ChannelID $.User.Mention }}
{{ $qsMessageID := sendMessageNoEscapeRetID .ChannelID (complexMessage "content" $vettingMessage "embed" (cembed "title" "Vetting questions" "description" $vettingQuestions "footer" (sdict "text" (print "Ticket expires at " (formatTime (currentTime.Add (toDuration "3d"))))))) }}
{{ sendMessage .ChannelID (cembed "author" (sdict "name" "Stuff for approvers...") "description" (print "* " (getMessage "vetting-logs" $logMessageID).Link "\n* <#1054180493144358952>") "image" (sdict "url" ($.User.AvatarURL "2048"))) }}
{{ $ticketInfo := sdict "UserID" .AuthorID "ChannelID" .ChannelID "LogMessageID" $logMessageID "QsMessageID" $qsMessageID }}
{{ dbSetExpire 0 (print "vet-chan-" .ChannelID) $ticketInfo 259300 }}
{{ dbSetExpire 0 (print "vet-user-" .AuthorID) $ticketInfo 259300 }}
{{ dbSetExpire 0 (print "vet-log-" $logMessageID) $ticketInfo 259300 }}
{{ scheduleUniqueCC 62 .ChannelID 259200 (print "vet-timeout-" .ChannelID) (sdict "TicketInfo" $ticketInfo "CloseReason" "Vetting channel inactive for too long.") }}
{{ end }}
{{ end }}
{{/* trigger on messages starting with the empty string in any channel */}}
{{ if hasSuffix .Channel.Name "vetting" }}
{{ with dbGet 0 (print "vet-chan-" .Channel.ID) }}
{{ scheduleUniqueCC 62 nil 259200 ( print "vet-timeout-" .Value.ChannelID ) (sdict "TicketInfo" .Value "CloseReason" "Vetting channel inactive for too long.") }}
{{ dbSetExpire 0 ( print "vet-chan-" .Value.ChannelID ) .Value 259300 }}
{{ dbSetExpire 0 ( print "vet-user-" .Value.UserID ) .Value 259300 }}
{{ dbSetExpire 0 ( print "vet-log-" .Value.LogMessageID ) .Value 259300 }}
{{/* update expiration time in question message footer */}}
{{ $embed := (structToSdict (index (getMessage nil .Value.QsMessageID).Embeds 0)) }}
{{ $embed.Set "footer" (sdict "text" (print "Ticket expires at " (formatTime (currentTime.Add (toDuration "3d"))))) }}
{{ editMessage nil .Value.QsMessageID (complexMessageEdit "embed" $embed) }}
{{ end }}
{{ end }}
{{/* trigger on reactions in any channel by an admin or approver */}}
{{ if or (eq .Channel.Name "vetting-logs") (hasSuffix .Channel.Name "vetting") }}
{{ if or (eq .Reaction.Emoji.Name "πŸ‘οΈ") (eq .Reaction.Emoji.Name "β˜‘οΈ") (eq .Reaction.Emoji.Name "❌") (eq .Reaction.Emoji.Name "πŸ‘’") }}
{{ $ticketInfoTry1 := dbGet 0 (print "vet-chan-" .Channel.ID) }}
{{ $ticketInfoTry2 := dbGet 0 (print "vet-log-" .ReactionMessage.ID) }}
{{ with or $ticketInfoTry1 $ticketInfoTry2 }}
{{ if eq $.Reaction.Emoji.Name "❌" }}
{{ sendMessage .Value.ChannelID (print "Denied and banned by " $.User.Mention) }}
{{ execAdmin "banid" .Value.UserID "24w" "vetting ban"}}
{{ execCC 62 .Value.ChannelID 0 (sdict "TicketInfo" .Value "CloseReason" (print "Denied/banned by " $.User.Username)) }}
{{ else if eq $.Reaction.Emoji.Name "πŸ‘’" }}
{{ sendMessage .Value.ChannelID (print "Kicked by " $.User.Mention) }}
{{ execAdmin "kick" .Value.UserID "vetting kick"}}
{{ execCC 62 .Value.ChannelID 0 (sdict "TicketInfo" .Value "CloseReason" (print "Kicked by " $.User.Username)) }}
{{ else }}
{{ if not (getMember .Value.UserID) }}
{{ execCC 62 nil 0 (cslice "TicketInfo" .Value "CloseReason" "Vetting approved but the user had already left the server πŸ˜“") }}
{{ else }}
{{ $foundPauseReact := false }}
{{ range (getMessage "vetting-logs" .Value.LogMessageID).Reactions }}
{{- if eq .Emoji.Name "⏸️" -}}
{{- $foundPauseReact = true -}}
{{- deleteMessageReaction nil $.ReactionMessage.ID $.User.ID $.Reaction.Emoji.Name -}}
{{- end -}}
{{ end }}
{{ if not $foundPauseReact }}
{{ giveRoleName .Value.UserID "vetted" }}
{{ $message := "" }}
{{ if eq $.Reaction.Emoji.Name "πŸ‘οΈ" }}
{{ $message = "You've been provisionally accepted, meaning that there's a πŸ‘οΈ next to your name to let people know you're still getting up to speed.\n\nTry to learn, keep an open mind, and have fun!\n\n**Please set your <#1088071957834104862>.**" }}
{{ giveRoleName .Value.UserID "liberal in the walls" }}
{{ end }}
{{ if eq $.Reaction.Emoji.Name "β˜‘οΈ" }}
{{ $message = "You can view #general now. Please self-set your Chattanoogan role (see <#1088071957834104862>) to view the rest of the server." }}
{{ end }}
{{/* Find the approver's dominant role color for the accent bar on the notice message. */}}
{{ $color := 0 }}
{{ $pos := 0 }}
{{ range $.Member.Roles }}
{{- with $.Guild.GetRole . -}}
{{- if and .Color (lt $pos .Position) -}}
{{- $color = .Color -}}
{{- $pos = .Position -}}
{{- end -}}
{{- end -}}
{{ end }}
{{ $user := userArg .Value.UserID }}
{{ $displayName := or (getMember .Value.UserID).Nick $user.Username }}
{{ sendMessageNoEscape "general"
(complexMessage
"content" $user.Mention
"embed" (cembed
"author" (sdict
"name" $.User.String
"icon_url" ($.User.AvatarURL "512")
)
"title" (print "Welcome " $displayName "!")
"color" $color
"thumbnail" (sdict "url" ($user.AvatarURL "512"))
"description" $message
)
) }}
{{ sendMessage .Value.ChannelID (print "Approved by " $.User.Mention) }}
{{ execCC 62 .Value.ChannelID 0 (sdict "TicketInfo" .Value "CloseReason" (print "Approved by " $.User.Username)) }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{/* trigger on none */}}
{{ with .ExecData }}
{{ editMessage "vetting-logs" .TicketInfo.LogMessageID (cembed "title" (printf "<@%s> vetting closed" (str .TicketInfo.UserID)) "description" .CloseReason) }}
{{ cancelScheduledUniqueCC 58 (print "vet-timeout-" .TicketInfo.ChannelID) }}
{{ dbDel 0 ( print "vet-chan-" .TicketInfo.ChannelID ) }}
{{ dbDel 0 ( print "vet-user-" .TicketInfo.UserID ) }}
{{ dbDel 0 ( print "vet-log-" .TicketInfo.LogMessageID ) }}
{{ if not .ChannelMissing }}
{{ exec "ticket close" .CloseReason }}
{{ end }}
{{ end }}
{{/* trigger on admin/approver -close */}}
{{ if hasSuffix .Channel.Name "vetting" }}
{{ with dbGet 0 (print "vet-chan-" .Channel.ID) }}
{{ execCC 62 nil 0 (sdict "TicketInfo" .Value "CloseReason" (print "Manually closed by " $.User.String)) }}
{{ else -}}
Please run this command from inside a valid vetting ticket.
{{- end }}
{{ end }}
{{/* trigger on 15min interval in vetting-logs */}}
{{ try }}
{{ $count := 100 }}
{{ $offset := 0 }}
{{ while eq $count 100 }}
{{ $rows := dbGetPattern 0 "vet-user-%" 100 $offset }}
{{ $count = len $rows }}
{{ $offset = add $offset $count }}
{{ range $rows }}
{{ if not (getMember .Value.UserID) }}
{{ if not (getChannel .Value.ChannelID) }}
{{ execCC 62 $.Channel.ID 0 (sdict "TicketInfo" .Value "CloseReason" "Left the server (ticket manually closed?)" "ChannelMissing" true) }}
{{ else }}
{{ execCC 62 .Value.ChannelID 0 (sdict "TicketInfo" .Value "CloseReason" "Left the server.") }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ catch }}
{{ if in .Error "execCC: too many calls" }}
_I tried closing tickets from users who left, but there were too many to do at once._
{{ else }}
Error: {{ .Error }}
{{ end }}
{{ end }}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment