Skip to content

Commit

Permalink
Initial commit of built-in glossary of terms, using define command
Browse files Browse the repository at this point in the history
Well, singular. Starting out with one term for the initial pass, and will
crowd-source more of them.

This also adds another method to the Responder, `RespondTo`, for mentioning the
user who triggered the command in the response.

Updates #6
  • Loading branch information
theckman committed Jun 5, 2020
1 parent 6bdca4b commit f4df3ee
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 26 deletions.
7 changes: 7 additions & 0 deletions cmd/consumer/consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/gobridge/gopherbot/cache"
"github.com/gobridge/gopherbot/cmd/consumer/playground"
"github.com/gobridge/gopherbot/config"
"github.com/gobridge/gopherbot/glossary"
"github.com/gobridge/gopherbot/handler"
"github.com/gobridge/gopherbot/internal/heartbeat"
"github.com/gobridge/gopherbot/workqueue"
Expand Down Expand Up @@ -135,6 +136,8 @@ func runServer(cfg config.C, logger zerolog.Logger) error {
return fmt.Errorf("failed to build MessageActions handler: %w", err)
}

gloss := glossary.New(glossary.Prefix)

tja := handler.NewTeamJoinActions(
shadowMode,
logger.With().Str("context", "team_join_actions").Logger(),
Expand All @@ -151,6 +154,10 @@ func runServer(cfg config.C, logger zerolog.Logger) error {
injectMessageReactions(ma)
injectMessageResponsePrefix(ma)

// handle "define " prefixed command
ma.HandlePrefix(glossary.Prefix, "find a definition in the glossary of Go-related terms", gloss.DefineHandler)

// set up the Go Playground uploader
lp := logger.With().Str("context", "playground")
pg := playground.New(newHTTPClient(), lp.Logger(), playgroundChannelBlacklist)
ma.HandleDynamic(pg.MessageMatchFn, pg.Handler)
Expand Down
74 changes: 74 additions & 0 deletions glossary/glossary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Package glossary provides some common terms that might be handy for a Gopher
// to know.
package glossary

import (
"fmt"
"strings"

"github.com/gobridge/gopherbot/handler"
"github.com/gobridge/gopherbot/workqueue"
)

// Prefix is the prefix that's intended to be used by the handler.
const Prefix = "define "

// Terms represents the glossary.
type Terms struct {
entries map[string][]string
aliases map[string]string
prefix string
}

// New generates a new set of glossary terms, from those it returns Terms.
func New(prefix string) Terms {
t := &Terms{
entries: make(map[string][]string),
aliases: make(map[string]string),
prefix: prefix,
}

for _, tfn := range terms {
tfn(t)
}

return *t
}

// DefineHandler satisfiees handler.MessageActionFn. It handles finding definitions for specific terms.
func (t Terms) DefineHandler(ctx workqueue.Context, m handler.Messenger, r handler.Responder) error {
if !m.BotMentioned() {
return nil
}

term := m.Text()[len(t.prefix):]

// this probably isn't possible with how Slack sends messages
// but let's have it just in case...
if len(term) == 0 {
return r.RespondTo(ctx, "You need to specify a term to define")
}

lterm := strings.ToLower(term)
lt := lterm

if v, ok := t.aliases[lterm]; ok {
lt = v
}

d, ok := t.entries[lt]
if !ok {
return r.RespondTo(ctx, "I'm sorry, I don't have a definition for that.")
}

ds := strings.Join(d, "\n")

var msg string
if lt != lterm { // alias was used
msg = fmt.Sprintf("`%s`, or `%s`, is %s", lt, lterm, ds)
} else {
msg = fmt.Sprintf("`%s` is %s", lt, ds)
}

return r.RespondMentions(ctx, msg)
}
35 changes: 35 additions & 0 deletions glossary/terms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package glossary

import "fmt"

var terms = []gOption{
// terms represents all the terms known by the glossary
//
// when adding items, please order alphabetically by the term

define("dependency injection", []string{"di"},
`a technique in which a type or function receives other things that it depends on, such as a database handler or logger`,
``,
"Note: my `dependency injection` command provides more details on how to use dependency injection in Go.",
),
}

type gOption func(t *Terms)

func define(term string, aliases []string, content ...string) gOption {
return func(t *Terms) {
if _, ok := t.entries[term]; ok {
panic(fmt.Sprintf("term %s already defined", term))
}

for _, a := range aliases {
if v, ok := t.aliases[a]; ok {
panic(fmt.Sprintf("alias %s already exists to %s", a, v))
}

t.aliases[a] = term
}

t.entries[term] = content
}
}
4 changes: 2 additions & 2 deletions handler/message_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func isDM(c ChannelType) bool {
// handlers are only invoked if the bot was mentioned.
func (m *MessageActions) Match(message Message) []MessageAction {
message.text, message.allMentions = mparser.ParseAndSplice(message.rawText, message.channelID)
message.text = strings.TrimSpace(message.text)
message.text = strings.TrimSpace(message.text) // Slack already trims the space off the end

message.userMentions, message.botMentioned = onlyOtherUserMMentions(m.selfID, message.allMentions)

Expand All @@ -239,7 +239,7 @@ func (m *MessageActions) Match(message Message) []MessageAction {

dm := isDM(message.channelType)

if dm || !m.shadowMode {
if dm || message.botMentioned || !m.shadowMode {
for k, v := range m.reactions {
if strings.Contains(lt, k) && (!v.onlyWhenMentioned || message.botMentioned) {
a := MessageAction{
Expand Down
53 changes: 29 additions & 24 deletions handler/responder.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ type Responder interface {

Respond(ctx context.Context, msg string, attachments ...slack.Attachment) error

// RespondTo is the same as respond, except it prefixes the message with an
// at-mention of the user who triggered the action. Helpful if responding
// with an error message.
RespondTo(ctx context.Context, msg string, attachments ...slack.Attachment) error

// RespondUnfurled is the same as Respond, except it asks slack to redner
// URL previews in the channel or DM.
RespondUnfurled(ctx context.Context, msg string, attachments ...slack.Attachment) error
Expand Down Expand Up @@ -70,56 +75,46 @@ func (r response) React(ctx context.Context, emoji string) error {
}

func (r response) Respond(ctx context.Context, msg string, attachments ...slack.Attachment) error {
return r.respond(ctx, false, false, false, r.m.channelID, r.m.threadTS, msg, attachments...)
return r.respond(ctx, false, false, false, false, r.m.channelID, r.m.threadTS, msg, attachments...)
}

func (r response) RespondTo(ctx context.Context, msg string, attachments ...slack.Attachment) error {
return r.respond(ctx, true, false, false, false, r.m.channelID, r.m.threadTS, msg, attachments...)
}

func (r response) RespondDM(ctx context.Context, msg string, attachments ...slack.Attachment) error {
return r.respond(ctx, false, false, false, r.m.userID, r.m.threadTS, msg, attachments...)
return r.respond(ctx, false, false, false, false, r.m.userID, r.m.threadTS, msg, attachments...)
}

func (r response) RespondUnfurled(ctx context.Context, msg string, attachments ...slack.Attachment) error {
return r.respond(ctx, false, false, true, r.m.channelID, r.m.threadTS, msg, attachments...)
return r.respond(ctx, false, false, false, true, r.m.channelID, r.m.threadTS, msg, attachments...)
}

func (r response) RespondTextAttachment(ctx context.Context, msg, attachment string) error {
return r.respond(ctx, false, false, false, r.m.channelID, r.m.threadTS, msg, slack.Attachment{Text: attachment})
return r.respond(ctx, false, false, false, false, r.m.channelID, r.m.threadTS, msg, slack.Attachment{Text: attachment})
}

func (r response) RespondMentions(ctx context.Context, msg string, attachments ...slack.Attachment) error {
return r.respond(ctx, true, false, false, r.m.channelID, r.m.threadTS, msg, attachments...)
return r.respond(ctx, false, true, false, false, r.m.channelID, r.m.threadTS, msg, attachments...)
}

func (r response) RespondMentionsUnfurled(ctx context.Context, msg string, attachments ...slack.Attachment) error {
return r.respond(ctx, true, false, true, r.m.channelID, r.m.threadTS, msg, attachments...)
return r.respond(ctx, false, true, false, true, r.m.channelID, r.m.threadTS, msg, attachments...)
}

func (r response) RespondMentionsTextAttachment(ctx context.Context, msg, attachment string) error {
return r.respond(ctx, true, false, false, r.m.channelID, r.m.threadTS, msg, slack.Attachment{Text: attachment})
return r.respond(ctx, false, true, false, false, r.m.channelID, r.m.threadTS, msg, slack.Attachment{Text: attachment})
}

func (r response) RespondEphemeral(ctx context.Context, msg string, attachments ...slack.Attachment) error {
m := mparser.Mention{
Type: mparser.TypeUser,
ID: r.m.userID,
}

msg = m.String() + " " + msg

return r.respond(ctx, false, true, false, r.m.channelID, r.m.threadTS, msg, attachments...)
return r.respond(ctx, true, false, true, false, r.m.channelID, r.m.threadTS, msg, attachments...)
}

func (r response) RespondEphemeralTextAttachment(ctx context.Context, msg, attachment string) error {
m := mparser.Mention{
Type: mparser.TypeUser,
ID: r.m.userID,
}

msg = m.String() + " " + msg

return r.respond(ctx, false, true, false, r.m.channelID, r.m.threadTS, msg, slack.Attachment{Text: attachment})
return r.respond(ctx, true, false, true, false, r.m.channelID, r.m.threadTS, msg, slack.Attachment{Text: attachment})
}

func (r response) respond(ctx context.Context, useMentions, ephemeral, unfurled bool, channelID, threadTS, msg string, attachments ...slack.Attachment) error {
func (r response) respond(ctx context.Context, mentionUser, useMentions, ephemeral, unfurled bool, channelID, threadTS, msg string, attachments ...slack.Attachment) error {
if useMentions && ephemeral {
return errors.New("cannot use mentions for ephemeral messages")
}
Expand All @@ -128,6 +123,16 @@ func (r response) respond(ctx context.Context, useMentions, ephemeral, unfurled
msg = mparser.Join(r.m.userMentions, " ") + msg
}

// do this after the above, so the original user is first in the message
if mentionUser {
u := mparser.Mention{
ID: r.m.userID,
Type: mparser.TypeUser,
}

msg = fmt.Sprintf("%s %s", u.String(), msg)
}

var opts []slack.MsgOption

if unfurled {
Expand Down

0 comments on commit f4df3ee

Please sign in to comment.