Skip to content

Commit

Permalink
fix a few Discord bugs
Browse files Browse the repository at this point in the history
Also move all message content to a messages map.
  • Loading branch information
kylrth committed Aug 29, 2023
1 parent 1804500 commit 6650340
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 60 deletions.
6 changes: 6 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"

"github.com/bwmarrin/discordgo"
"github.com/cobaltspeech/log"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
Expand Down Expand Up @@ -93,6 +94,11 @@ func addGuildInfo(l log.Logger, bot *bouncerbot.Bot) error {
if err != nil {
if errors.Is(err, os.ErrNotExist) {
l.Info("msg", "guild info file does not exist")

// We'll set the bot to listen for guild messages this time, so we can retrieve the
// guild info as soon as possible.
bot.Identify.Intents |= discordgo.IntentGuildMessages
// We'll give the bot a callback that saves the guild info to disk once it's received.
bot.AddGuildInfoCallback(saveGuildInfo(l))

return nil
Expand Down
8 changes: 8 additions & 0 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ func TestUploadAndDecrypt(t *testing.T) { //nolint:cyclop,funlen,gocyclo // long
if diff := cmp.Diff(&u1, out); diff != "" {
t.Error("unexpected decrypted info (-want +got):\n" + diff)
}
err = dec.Delete(u1.ID)
if err != nil {
t.Errorf("unexpected error from Decrypter.Delete: %v", err)
}

// (try again, should get ErrNotFound)
_, err = dec.Decrypt(u1Key)
Expand All @@ -405,6 +409,10 @@ func TestUploadAndDecrypt(t *testing.T) { //nolint:cyclop,funlen,gocyclo // long
if diff := cmp.Diff(&u2, out); diff != "" {
t.Error("unexpected decrypted info (-want +got):\n" + diff)
}
err = dec.Delete(u2.ID)
if err != nil {
t.Errorf("unexpected error from Decrypter.Delete: %v", err)
}

// now it should be empty
users, err = c.Users.GetAllUsers(ctx)
Expand Down
168 changes: 116 additions & 52 deletions pkg/bouncerbot/bouncerbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ func NewWithDecrypter(l log.Logger, token string, d Decrypter) (*Bot, error) {
return &b, err
}

b.AddHandler(b.memberJoin)
b.AddHandler(b.userDM)
dg.Identify.Intents = 0

b.AddHandler(b.handleMemberJoin)
dg.Identify.Intents |= discordgo.IntentGuildMembers

b.AddHandler(b.handleMessage)
dg.Identify.Intents |= discordgo.IntentDirectMessages

return &b, nil
}
Expand All @@ -57,27 +62,9 @@ func (b *Bot) AddGuildInfoCallback(f func(*GuildInfo)) {
b.guildInfoCallbacks = append(b.guildInfoCallbacks, f)
}

var welcomeMessages = []string{
"Welcome to the ACME Discord server! Please send me your unique code here to gain access to " +
"the rest of the server.",
"By sending your code, you're allowing BYU to give the server admins your real name and the " +
"year you finished the senior cohort.",
"I'll use this information to set which channels you'll be able to see, and to set your " +
"nickname on the server.",
}

func (b *Bot) memberJoin(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
func (b *Bot) handleMemberJoin(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
if b.Guild == nil {
roles, err := s.GuildRoles(m.GuildID)
if err != nil {
b.l.Error("msg", "failed to get guild info", "error", err)
}

b.Guild = GetGuildInfo(b.l, roles, m.GuildID)

for _, cb := range b.guildInfoCallbacks {
cb(b.Guild)
}
b.setGuildInfo(m.GuildID)
}

channel, err := s.UserChannelCreate(m.User.ID)
Expand All @@ -87,16 +74,34 @@ func (b *Bot) memberJoin(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
return
}

for _, msg := range welcomeMessages {
b.message(channel.ID, "send welcome message", msg)
b.message(channel.ID, messageWelcome)
b.l.Debug("msg", "sent welcome DM", "user", m.User.ID, "username", m.User.Username)
}

func (b *Bot) setGuildInfo(guildID string) {
roles, err := b.GuildRoles(guildID)
if err != nil {
b.l.Error("msg", "failed to get guild roles", "error", err)

return
}

b.l.Debug("msg", "sent welcome DM", "user", m.User.ID, "username", m.User.Username)
b.Guild = GetGuildInfo(b.l, roles, guildID)

for _, cb := range b.guildInfoCallbacks {
cb(b.Guild)
}
}

func (b *Bot) userDM(s *discordgo.Session, m *discordgo.MessageCreate) {
func (b *Bot) handleMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
if m.GuildID != "" {
// not a DM

if b.Guild == nil {
// let's get the guild info while we're here
b.setGuildInfo(m.GuildID)
}

return
}
if m.Author.ID == s.State.User.ID {
Expand All @@ -108,49 +113,104 @@ func (b *Bot) userDM(s *discordgo.Session, m *discordgo.MessageCreate) {
if err != nil {
if errors.As(err, &encrypt.ErrBadKey{}) {
b.l.Info("msg", "DM did not provide acceptable key", "key", m.Content, "error", err)

b.message(m.ChannelID, "reply to bad key",
"Sorry, that key did not work. Send just the key in plain text, nothing else.",
)
b.message(m.ChannelID, messageBadKey)

return
}
if errors.Is(err, ErrNotFound) {
b.l.Info("msg", "key did not decrypt any current user", "key", m.Content, "error", err)

b.message(m.ChannelID, "reply to unused key",
"Sorry, that key did not work. Reach out to the admins for help!",
)
b.message(m.ChannelID, messageNotFound)

return
}

b.l.Error("msg", "error decrypting with key", "key", m.Content, "error", err)
b.message(m.ChannelID, messageDecryptionError)

b.message(m.ChannelID, "reply about decryption error",
"There was an error on my end (`"+err.Error()+"`). Reach out to the admins for help!",
)
return
}

b.message(m.ChannelID, "inform about successful decryption",
"I found your info! I'll let you in now. :)",
)
b.message(m.ChannelID, messageSuccessful)

err = b.admit(u, m.Author.ID)
if err != nil {
b.l.Error("msg", "failed to admit new user", "error", err)
b.message(m.ChannelID, "reply about admit error",
"There was an error on my end (`"+err.Error()+"`). Reach out to the admins for help!",
)
if err.Error() != errNick403 {
b.l.Error("msg", "failed to admit new user", "error", err)
b.message(m.ChannelID, messageAdmitError)

return
}

b.message(m.ChannelID, messageNickPerm)
}

b.l.Info("msg", "admitted new user", "userID", m.Author.ID, "username", m.Author.Username)
}

func (b *Bot) message(channelID, whatFor, msg string) {
_, err := b.ChannelMessageSend(channelID, msg)
// Delete the user now that we've successfully admitted them.
err = b.d.Delete(u.ID)
if err != nil {
b.l.Error("msg", "failed to "+whatFor, "error", err)
b.l.Error("msg", "failed to delete user after admitting", "id", u.ID)
}
}

const (
messageWelcome = "send welcome message"
messageSuccessful = "inform about successful decryption"
messageBadKey = "reply to bad key"
messageNotFound = "reply to unused key"
messageDecryptionError = "reply about decryption error"
messageNickPerm = "reply about nickname permissions"
messageAdmitError = "reply about admit error"
messageOtherError = "inform about internal error"
)

var messages = map[string][]string{
messageWelcome: {
"Welcome to the ACME Discord server! Please send me your unique code here to gain access " +
"to the rest of the server.",
"By sending your code, you're allowing BYU to give the server admins your real name and " +
"the year you finished the senior cohort.",
"I'll use this information to set which channels you'll be able to see, and to set your " +
"nickname on the server.",
},
messageSuccessful: {"I found your info! I'll let you in now. :)"},
messageBadKey: {
"Sorry, that key did not work. The key should be 64 hexadecimal characters, sent as " +
"plain text a single message by itself.",
"If you still have trouble, reach out to the admins for help.",
},
messageNotFound: {"Sorry, that key did not work. Reach out to the admins for help!"},
messageDecryptionError: {
"There was a decryption error with that key. Reach out to the admins for help!",
},
messageNickPerm: {
"Everything worked except I wasn't able to set your nickname because of your high role.",
"Please set your nickname by sending `/nick FIRST LAST` in one of the channels.",
},
messageAdmitError: {
"There was an error while trying to admit you. Reach out to the admins for help!",
},
messageOtherError: {
"There was an error with a message I tried to send. Reach out to the admins to complain!",
},
}

func (b *Bot) message(channelID, whatFor string) {
msgs, ok := messages[whatFor]
if !ok {
b.l.Error("msg", "unknown message type", "type", whatFor)
b.message(channelID, messageOtherError)

return
}

for _, msg := range msgs {
_, err := b.ChannelMessageSend(channelID, msg)
if err != nil {
b.l.Error("msg", "failed to "+whatFor, "error", err)

break
}
}
}

Expand All @@ -159,24 +219,28 @@ func (b *Bot) admit(u *db.User, dID string) error {
return errors.New("guild info not discovered yet")
}

var errs []error

err := b.GuildMemberNickname(b.Guild.GuildID, dID, u.Name)
if err != nil {
return fmt.Errorf("set nick: %w", err)
errs = append(errs, fmt.Errorf("set nick: %w", err))
}

rolesToAdd := b.Guild.GetRoleIDsForUser(b.l, u)

for _, roleID := range rolesToAdd {
err = b.GuildMemberRoleAdd(b.Guild.GuildID, dID, roleID)
if err != nil {
return fmt.Errorf("set role: %w", err)
errs = append(errs, fmt.Errorf("set role '%s': %w", roleID, err))
}
}

err = b.GuildMemberRoleRemove(b.Guild.GuildID, dID, b.Guild.NewbieRole)
if err != nil {
return fmt.Errorf("remove newbie role: %w", err)
errs = append(errs, fmt.Errorf("remove newbie role: %w", err))
}

return nil
return errors.Join(errs...)
}

const errNick403 = `set nick: HTTP 403 Forbidden, {"message": "Missing Permissions", "code": 50013}`
18 changes: 10 additions & 8 deletions pkg/bouncerbot/decrypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,25 @@ type Decrypter interface {
// Decrypt attempts to decrypt any user info using the key provided. It returns ErrNotFound if
// the key did not decrypt anything.
Decrypt(key string) (*db.User, error)

// Delete removes the user info after it's been decrypted and used. It should be called only
// after the successful use of data returned by Decrypt.
Delete(id int) error
}

// ErrNotFound is returned by a Decrypter if the key did not decrypt any info.
var ErrNotFound = errors.New("user info not found")

// TableDecrypter implements Decrypter by using the key to attempt to decrypt all the users in the
// database. If the user is found, it is deleted from the table.
// database.
type TableDecrypter struct {
Table *db.UserTable
}

func (d TableDecrypter) Decrypt(key string) (*db.User, error) {
keyHash, err := encrypt.MD5Hash(key)
if err != nil {
return nil, err
return nil, encrypt.NewErrBadKey(err)
}

users, err := d.Table.GetUsers(context.Background(), db.WithKeyHash(keyHash))
Expand All @@ -47,14 +51,12 @@ func (d TableDecrypter) Decrypt(key string) (*db.User, error) {
continue
}

// Delete the user from the table.
err = d.Table.DeleteUser(context.Background(), user.ID)
if err != nil {
return user, fmt.Errorf("delete user after decryption: %w", err)
}

return user, nil
}

return nil, ErrNotFound
}

func (d TableDecrypter) Delete(id int) error {
return d.Table.DeleteUser(context.Background(), id)
}
5 changes: 5 additions & 0 deletions pkg/encrypt/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ type ErrBadKey struct {
error
}

// NewErrBadKey wraps the given error to signify that this error was caused by a bad key.
func NewErrBadKey(wrap error) ErrBadKey {
return ErrBadKey{wrap}
}

// ErrInauthenticated is returned when the key was not the correct one for the ciphertext.
type ErrInauthenticated struct {
error
Expand Down
15 changes: 15 additions & 0 deletions pkg/encrypt/encrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package encrypt_test
import (
crand "crypto/rand"
"encoding/hex"
"errors"
"fmt"
"math/rand"
"strconv"
Expand Down Expand Up @@ -186,3 +187,17 @@ func TestFromJS(t *testing.T) {
t.Error("unexpected output (-want +got):\n" + diff)
}
}

func TestDecrypt_Errors(t *testing.T) {
t.Parallel()

_, err := encrypt.Decrypt("asdfjkl", key)
if !errors.As(err, &encrypt.ErrBadCiphertext{}) {
t.Errorf("error did not match ErrBadCiphertext: %v", err)
}

_, err = encrypt.Decrypt(ciphertext, "asdfjkl")
if !errors.As(err, &encrypt.ErrBadKey{}) {
t.Errorf("error did not match ErrBadKey: %v", err)
}
}

0 comments on commit 6650340

Please sign in to comment.