Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add template support #285

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion webhookbot/db.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ CREATE TABLE `hooks` (
`id` varchar(100) NOT NULL,
`name` varchar(100) NOT NULL,
`conv_id` varchar(100) NOT NULL,
`template` varchar(10000) NOT NULL,
CONSTRAINT hook UNIQUE (`name`,`conv_id`),
PRIMARY KEY (`id`),
KEY `conv_id` (`conv_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
32 changes: 29 additions & 3 deletions webhookbot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,20 @@ const back = "`"
const backs = "```"

func (s *BotServer) makeAdvertisement() kbchat.Advertisement {
createExtended := fmt.Sprintf(`Create a new webhook for sending messages into the current conversation. You must supply a name as well to identify the webhook. To use a webhook URL, supply a %smsg%s URL parameter, or a JSON POST body with a field %smsg%s.
createExtended := fmt.Sprintf(`Create a new webhook for sending messages into the current conversation. You must supply a name as well to identify the webhook. To use a webhook URL, supply a %smsg%s URL parameter, or a JSON POST body with a field %smsg%s. You can also supply a template, which allows you to customize the message displayed by the webhook, and the URL and/or JSON fields it will accept. For more information on templates, use the %s!webhook help%s command.

Example:%s
!webhook create alerts%s`, back, back, back, back, backs, backs)
!webhook create alerts%s

Example (using custom template):%s
!webhook create alerts *{{.title}}*
%s{{.body}}%s%s`,
back, back, back, back, back, back, backs, backs, backs, back, back, backs)
updateExtended := fmt.Sprintf(`Update an existing webhook's template. Leave the template field empty to use the default template. For more information on templates, use the %s!webhook help%s command.

Example:%s
!webhook update alerts *New Alert: {{.title}}*
%s{{.body}}%s%s`, back, back, backs, back, back, backs)
removeExtended := fmt.Sprintf(`Remove a webhook from the current conversation. You must supply the name of the webhook.

Example:%s
Expand All @@ -57,14 +67,26 @@ func (s *BotServer) makeAdvertisement() kbchat.Advertisement {
cmds := []chat1.UserBotCommandInput{
{
Name: "webhook create",
Usage: "<name> [<template>]",
Description: "Create a new webhook for sending into the current conversation",
ExtendedDescription: &chat1.UserBotExtendedDescription{
Title: `*!webhook create* <name>
Title: `*!webhook create* <name> [<template>]
Create a webhook`,
DesktopBody: createExtended,
MobileBody: createExtended,
},
},
{
Name: "webhook update",
Usage: "<name> [<template>]",
Description: "Update the template of an existing webhook in the current conversation",
ExtendedDescription: &chat1.UserBotExtendedDescription{
Title: `*!webhook update* <name> [<template>]
Update a webhook's template`,
DesktopBody: updateExtended,
MobileBody: updateExtended,
},
},
{
Name: "webhook list",
Description: "List active webhooks in the current conversation",
Expand All @@ -79,6 +101,10 @@ Remove a webhook`,
MobileBody: removeExtended,
},
},
{
Name: "webhook help",
Description: "Get more information about using templates",
},
base.GetFeedbackCommandAdvertisement(s.kbc.GetUsername()),
}
return kbchat.Advertisement{
Expand Down
33 changes: 24 additions & 9 deletions webhookbot/webhookbot/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,54 @@ func (d *DB) makeID(name string, convID chat1.ConvIDStr) (string, error) {
return base.URLEncoder().EncodeToString(h.Sum(nil)[:20]), nil
}

func (d *DB) Create(name string, convID chat1.ConvIDStr) (string, error) {
func (d *DB) Create(name, template string, convID chat1.ConvIDStr) (string, error) {
id, err := d.makeID(name, convID)
if err != nil {
return "", err
}
err = d.RunTxn(func(tx *sql.Tx) error {
if _, err := tx.Exec(`
INSERT INTO hooks
(id, name, conv_id)
(id, name, template, conv_id)
VALUES
(?, ?, ?)
`, id, name, convID); err != nil {
(?, ?, ?, ?)
`, id, name, template, convID); err != nil {
return err
}
return nil
})
return id, err
}

func (d *DB) Update(name, template string, convID chat1.ConvIDStr) error {
err := d.RunTxn(func(tx *sql.Tx) error {
if _, err := tx.Exec(`
UPDATE hooks
SET template = ?
WHERE conv_id = ? AND name = ?
`, template, convID, name); err != nil {
return err
}
return nil
})
return err
}

func (d *DB) GetHook(id string) (res webhook, err error) {
row := d.DB.QueryRow(`
SELECT conv_id, name FROM hooks WHERE id = ?
SELECT conv_id, name, template FROM hooks WHERE id = ?
`, id)
if err := row.Scan(&res.convID, &res.name); err != nil {
if err := row.Scan(&res.convID, &res.name, &res.template); err != nil {
return res, err
}
return res, nil
}

type webhook struct {
id string
convID chat1.ConvIDStr
name string
id string
convID chat1.ConvIDStr
name string
template string
}

func (d *DB) List(convID chat1.ConvIDStr) (res []webhook, err error) {
Expand Down
108 changes: 104 additions & 4 deletions webhookbot/webhookbot/handler.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package webhookbot

import (
"database/sql"
"errors"
"fmt"
"strings"
"text/template"

_ "github.com/go-sql-driver/mysql"
"github.com/keybase/go-keybase-chat-bot/kbchat"
"github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1"
"github.com/keybase/managed-bots/base"
)

const defaultTemplate = "[hook: *{{$webhookName}}*]\n\n{{if eq $webhookMethod \"POST\"}}{{.msg}}{{else}}{{index .msg 0}}{{end}}"

type Handler struct {
*base.DebugOutput

Expand Down Expand Up @@ -54,7 +58,7 @@ func (h *Handler) checkAllowed(msg chat1.MsgSummary) error {

func (h *Handler) handleRemove(cmd string, msg chat1.MsgSummary) (err error) {
convID := msg.ConvID
toks := strings.Split(cmd, " ")
toks := strings.Fields(cmd)
if len(toks) != 3 {
h.ChatEcho(convID, "invalid number of arguments, must specify a name")
return nil
Expand Down Expand Up @@ -109,10 +113,39 @@ func (h *Handler) handleList(cmd string, msg chat1.MsgSummary) (err error) {
return nil
}

func validateTemplate(cmd string) (templateSrc string, err error) {
toks := strings.Fields(cmd)

var trigger string
switch toks[1] {
case "create":
trigger = "!webhook create"
case "update":
trigger = "!webhook update"
}

// templateSrc is whatever remains after removing the trigger (i.e `!webhook create` or
// `!webhook update`) followed by the template name, and trimming spaces. if the template
// is empty, we'll set a default one
name := toks[2]
templateSrc = strings.Replace(cmd, trigger, "", 1)
templateSrc = strings.Replace(templateSrc, name, "", 1)
templateSrc = strings.TrimSpace(templateSrc)
if templateSrc == "" {
templateSrc = defaultTemplate
}
tWithVars := injectTemplateVars("testhook1", "POST", templateSrc)
_, err = template.New("").Parse(tWithVars)
if err != nil {
return "", err
}
return templateSrc, nil
}

func (h *Handler) handleCreate(cmd string, msg chat1.MsgSummary) (err error) {
convID := msg.ConvID
toks := strings.Split(cmd, " ")
if len(toks) != 3 {
toks := strings.Fields(cmd)
if len(toks) < 3 {
h.ChatEcho(convID, "invalid number of arguments, must specify a name")
return nil
}
Expand All @@ -128,7 +161,13 @@ func (h *Handler) handleCreate(cmd string, msg chat1.MsgSummary) (err error) {

h.stats.Count("create")
name := toks[2]
id, err := h.db.Create(name, convID)
templateSrc, err := validateTemplate(cmd)
if err != nil {
h.ChatEcho(convID, "failed to parse template: %v", err)
return fmt.Errorf("handleCreate: failed to parse template: %s", err)
}

id, err := h.db.Create(name, templateSrc, convID)
if err != nil {
return fmt.Errorf("handleCreate: failed to create webhook: %s", err)
}
Expand All @@ -139,6 +178,63 @@ func (h *Handler) handleCreate(cmd string, msg chat1.MsgSummary) (err error) {
return nil
}

func (h *Handler) handleUpdate(cmd string, msg chat1.MsgSummary) (err error) {
convID := msg.ConvID
toks := strings.Fields(cmd)
if len(toks) < 3 {
h.ChatEcho(convID, "invalid number of arguments, must specify a name")
return nil
}
err = h.checkAllowed(msg)
switch err {
case nil:
case errNotAllowed:
h.ChatEcho(convID, err.Error())
return nil
default:
return err
}
h.stats.Count("update")
name := toks[2]
templateSrc, err := validateTemplate(cmd)
if err != nil {
h.ChatEcho(convID, "failed to parse template: %v", err)
return fmt.Errorf("handleUpdate: failed to parse template: %s", err)
}

err = h.db.Update(name, templateSrc, convID)
switch err {
case nil:
case sql.ErrNoRows:
default:
h.ChatEcho(convID, "failed to update template: no webhook with that name exists in this conversation. a new webhook can be created with the `!webhook create` command.")
return nil
}
h.ChatEcho(convID, "Success!")
return nil
}

func (h *Handler) handleHelp(msg chat1.MsgSummary) (err error) {
h.stats.Count("help")
back := "`"
backs := "```"
convID := msg.ConvID
h.ChatEcho(convID, `
*Templates:*
- Information about creating templates, and some examples can be found at https://pkg.go.dev/text/template
- You can test your templates against your JSON data at https://play.golang.org/p/vC06kRCQDfX

There are 2 custom variables you can use in your templates (see the default template below for example usage):
- %s$webhookName%s: The name you gave the webhook
- %s$webhookMethod%s: Whether GET or POST was used when the webhook endpoint was fetched

If you don't provide a template, this is the default:%s
[hook: *{{$webhookName}}*]

{{if eq $webhookMethod "POST"}}{{.msg}}{{else}}{{index .msg 0}}{{end}}%s`, back, back, back, back, backs, backs)
return nil
}

func (h *Handler) HandleNewConv(conv chat1.ConvSummary) error {
welcomeMsg := "I can create generic webhooks into Keybase! Try `!webhook create` to get started."
return base.HandleNewTeam(h.stats, h.DebugOutput, h.kbc, conv, welcomeMsg)
Expand All @@ -156,6 +252,10 @@ func (h *Handler) HandleCommand(msg chat1.MsgSummary) error {
return h.handleList(cmd, msg)
case strings.HasPrefix(cmd, "!webhook remove"):
return h.handleRemove(cmd, msg)
case strings.HasPrefix(cmd, "!webhook update"):
return h.handleUpdate(cmd, msg)
case strings.HasPrefix(cmd, "!webhook help"):
return h.handleHelp(msg)
}
return nil
}
Loading