diff --git a/README.md b/README.md index 726b573..6b7cc9b 100755 --- a/README.md +++ b/README.md @@ -30,7 +30,9 @@ go run ./cmd/frodo-bot ### 3. Supported Commands -nothing yet +| Command | Description | +| ------------- | ------------------------------------------------------------------- | +| /morgul-blade | Stab Frodo with the cursed Morgul blade to silence him indefinitely | Press `Ctrl+C` to stop. diff --git a/cmd/frodo-bot/main.go b/cmd/frodo-bot/main.go index b94b6d0..c315001 100644 --- a/cmd/frodo-bot/main.go +++ b/cmd/frodo-bot/main.go @@ -24,22 +24,22 @@ func run() error { return fmt.Errorf("config failed: %w", err) } - discord, err := discordgo.New("Bot " + cfg.Token) + session, err := discordgo.New("Bot " + cfg.Token) if err != nil { return fmt.Errorf("bot failed to init: %w", err) } b := bot.New(cfg) - b.Register(discord) + b.Register(session) // open session - err = discord.Open() + err = session.Open() if err != nil { return fmt.Errorf("failed to open discord session: %w", err) } // executes after run returns - defer discord.Close() + defer session.Close() fmt.Println("bot is running") c := make(chan os.Signal, 1) diff --git a/internal/bot/assets/morgul-blade.png b/internal/bot/assets/morgul-blade.png new file mode 100755 index 0000000..088ab9c Binary files /dev/null and b/internal/bot/assets/morgul-blade.png differ diff --git a/internal/bot/bot.go b/internal/bot/bot.go index 3b61bf6..392e68a 100644 --- a/internal/bot/bot.go +++ b/internal/bot/bot.go @@ -4,24 +4,35 @@ import ( "log" "github.com/SimonKimDev/frodo-bot/internal/config" + "github.com/SimonKimDev/frodo-bot/internal/wound" "github.com/bwmarrin/discordgo" ) type Bot struct { gifURL string + wounds *wound.Tracker } func New(cfg config.Config) *Bot { - return &Bot{gifURL: cfg.GIFURL} + return &Bot{ + wounds: wound.New(), + gifURL: cfg.GIFURL, + } } +// Register adds the bot's event handlers and intents to the current Session. +// Should be called before calling session.Open func (b *Bot) Register(s *discordgo.Session) { s.AddHandler(b.onReady) s.AddHandler(b.onMessage) + s.AddHandler(b.onInteraction) s.Identify.Intents = discordgo.IntentsMessageContent | discordgo.IntentsGuildMessages } func (b *Bot) onReady(s *discordgo.Session, r *discordgo.Ready) { user := r.User log.Printf("logged in as %v", user.Username) + if _, err := s.ApplicationCommandCreate(user.ID, "", morgulBladeCommand()); err != nil { + log.Printf("failed to create command %v", err) + } } diff --git a/internal/bot/secret.go b/internal/bot/messagehandler.go similarity index 82% rename from internal/bot/secret.go rename to internal/bot/messagehandler.go index af532e5..719fb58 100644 --- a/internal/bot/secret.go +++ b/internal/bot/messagehandler.go @@ -12,6 +12,10 @@ func (b *Bot) onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { return } + if b.wounds.IsStabbed(m.GuildID) { + return + } + if !shouldReply(m.Content) { return } @@ -21,6 +25,7 @@ func (b *Bot) onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { } } +// shouldReply reports whether to post the meme or not. func shouldReply(content string) bool { return strings.Contains(strings.ToLower(content), "secret") } diff --git a/internal/bot/morgulblade.go b/internal/bot/morgulblade.go new file mode 100644 index 0000000..df4200d --- /dev/null +++ b/internal/bot/morgulblade.go @@ -0,0 +1,61 @@ +package bot + +import ( + "bytes" + _ "embed" + "log" + + "github.com/bwmarrin/discordgo" +) + +const morgulBladeCmd = "morgul-blade" + +//go:embed assets/morgul-blade.png +var morgulBladePNG []byte + +func morgulBladeCommand() *discordgo.ApplicationCommand { + return &discordgo.ApplicationCommand{ + Name: morgulBladeCmd, + Description: "Stabs Frodo with the Morgul Blade to silence him indefinitely", + Contexts: &[]discordgo.InteractionContextType{discordgo.InteractionContextGuild}, + } +} + +func (b *Bot) onInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { + if i.Type != discordgo.InteractionApplicationCommand { + return + } + + if i.ApplicationCommandData().Name != morgulBladeCmd { + return + } + + if !b.wounds.Stab(i.GuildID) { + respond(s, i, &discordgo.InteractionResponseData{ + Content: "You awful person, Frodo has already been stabbed.", + Flags: discordgo.MessageFlagsEphemeral, + }) + return + } + + respond(s, i, &discordgo.InteractionResponseData{ + Content: "DEUUEAUGH!!", + Files: []*discordgo.File{ + { + Name: "morgul-blade.png", + ContentType: "image/png", + Reader: bytes.NewReader(morgulBladePNG), + }, + }, + }) +} + +func respond(s *discordgo.Session, i *discordgo.InteractionCreate, data *discordgo.InteractionResponseData) { + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: data, + }) + if err != nil { + log.Printf("respond to /%s: %v", i.ApplicationCommandData().Name, err) + } +} diff --git a/internal/wound/wound.go b/internal/wound/wound.go new file mode 100644 index 0000000..115dd6f --- /dev/null +++ b/internal/wound/wound.go @@ -0,0 +1,38 @@ +package wound + +import ( + "sync" +) + +// Tracker records which server (guild) have Frodo stabbed. Monsters... +// It's safe for concurrent use. +type Tracker struct { + mu sync.Mutex + stabbed map[string]bool +} + +func New() *Tracker { + return &Tracker{stabbed: make(map[string]bool)} +} + +// Stab marks guildID as stabbed return false if already stabbed +// Because it wouldn't make sense to stab Frodo multiple times. +// Come on, he's already wounded. +func (t *Tracker) Stab(guildID string) bool { + t.mu.Lock() + defer t.mu.Unlock() + + if t.stabbed[guildID] { + return false + } + + t.stabbed[guildID] = true + return true +} + +// IsStabbed reports whether Frodo was already stabbed or not +func (t *Tracker) IsStabbed(guildID string) bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.stabbed[guildID] +}