Metadata for Webhooks

Added the `META name value` keyword to the SETHOOK command.

Allows for adding metadata to a webhook. For example:

    SETHOOK myhook http://endpoint/ META m1 12 META m2 13 NEARBY ...

Would result in notification that contain the "meta" element, which is
represented like:

    "meta":{"m1":"12","m2":"13"}

Thanks for the suggestion @amorskoy

closed #105
This commit is contained in:
Josh Baker 2016-12-29 08:50:54 -07:00
parent 73fd3cf7de
commit bafb1823b3
7 changed files with 136 additions and 55 deletions

View File

@ -32,7 +32,7 @@ var (
quiet bool quiet bool
) )
// Fire up a webhook test server by using the --webhook-consumer-http-port // Fire up a webhook test server by using the --webhook-http-consumer-port
// for example // for example
// $ ./tile38-server --webhook-http-consumer-port 9999 // $ ./tile38-server --webhook-http-consumer-port 9999
// //

View File

@ -229,7 +229,7 @@ func (c *Controller) queueHooks(d *commandDetailsT) error {
if hm, ok := c.hookcols[d.key]; ok { if hm, ok := c.hookcols[d.key]; ok {
for _, hook := range hm { for _, hook := range hm {
// match the fence // match the fence
msgs := FenceMatch(hook.Name, hook.ScanWriter, hook.Fence, d) msgs := FenceMatch(hook.Name, hook.ScanWriter, hook.Fence, hook.Metas, d)
if len(msgs) > 0 { if len(msgs) > 0 {
// append each msg to the big list // append each msg to the big list
hmsgs = append(hmsgs, msgs...) hmsgs = append(hmsgs, msgs...)

View File

@ -1,7 +1,6 @@
package controller package controller
import ( import (
"fmt"
"math" "math"
"strconv" "strconv"
"time" "time"
@ -13,13 +12,8 @@ import (
) )
// FenceMatch executes a fence match returns back json messages for fence detection. // FenceMatch executes a fence match returns back json messages for fence detection.
func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, details *commandDetailsT) [][]byte { func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas []FenceMeta, details *commandDetailsT) [][]byte {
overall := time.Now() msgs := fenceMatch(hookName, sw, fence, metas, details)
defer func() {
return
fmt.Printf(">> %v\n", time.Since(overall))
}()
msgs := fenceMatch(hookName, sw, fence, details)
if len(fence.accept) == 0 { if len(fence.accept) == 0 {
return msgs return msgs
} }
@ -42,10 +36,31 @@ func jsonTimeFormat(t time.Time) string {
b = appendJSONTimeFormat(b, t) b = appendJSONTimeFormat(b, t)
return string(b) return string(b)
} }
func appendHookDetails(b []byte, hookName string, metas []FenceMeta) []byte {
func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, details *commandDetailsT) [][]byte { if len(hookName) > 0 {
b = append(b, `,"hook":`...)
b = appendJSONString(b, hookName)
}
if len(metas) > 0 {
b = append(b, `,"meta":{`...)
for i, meta := range metas {
if i > 0 {
b = append(b, ',')
}
b = appendJSONString(b, meta.Name)
b = append(b, ':')
b = appendJSONString(b, meta.Value)
}
b = append(b, '}')
}
return b
}
func hookJSONString(hookName string, metas []FenceMeta) string {
return string(appendHookDetails(nil, hookName, metas))
}
func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas []FenceMeta, details *commandDetailsT) [][]byte {
if details.command == "drop" { if details.command == "drop" {
return [][]byte{[]byte(`{"command":"drop","hook":` + jsonString(hookName) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`)} return [][]byte{[]byte(`{"command":"drop"` + hookJSONString(hookName, metas) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`)}
} }
if len(fence.glob) > 0 && !(len(fence.glob) == 1 && fence.glob[0] == '*') { if len(fence.glob) > 0 && !(len(fence.glob) == 1 && fence.glob[0] == '*') {
match, _ := glob.Match(fence.glob, details.id) match, _ := glob.Match(fence.glob, details.id)
@ -65,7 +80,7 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai
} }
} }
if details.command == "del" { if details.command == "del" {
return [][]byte{[]byte(`{"command":"del","hook":` + jsonString(hookName) + `,"id":` + jsonString(details.id) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`)} return [][]byte{[]byte(`{"command":"del"` + hookJSONString(hookName, metas) + `,"id":` + jsonString(details.id) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`)}
} }
var roamkeys, roamids []string var roamkeys, roamids []string
@ -174,18 +189,18 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai
msgs := make([][]byte, 0, 4) msgs := make([][]byte, 0, 4)
if fence.detect == nil || fence.detect[detect] { if fence.detect == nil || fence.detect[detect] {
if len(res) > 0 && res[0] == '{' { if len(res) > 0 && res[0] == '{' {
res = makemsg(details.command, group, detect, hookName, details.key, details.timestamp, res[1:]) res = makemsg(details.command, group, detect, hookName, metas, details.key, details.timestamp, res[1:])
} }
msgs = append(msgs, res) msgs = append(msgs, res)
} }
switch detect { switch detect {
case "enter": case "enter":
if fence.detect == nil || fence.detect["inside"] { if fence.detect == nil || fence.detect["inside"] {
msgs = append(msgs, makemsg(details.command, group, "inside", hookName, details.key, details.timestamp, res[1:])) msgs = append(msgs, makemsg(details.command, group, "inside", hookName, metas, details.key, details.timestamp, res[1:]))
} }
case "exit", "cross": case "exit", "cross":
if fence.detect == nil || fence.detect["outside"] { if fence.detect == nil || fence.detect["outside"] {
msgs = append(msgs, makemsg(details.command, group, "outside", hookName, details.key, details.timestamp, res[1:])) msgs = append(msgs, makemsg(details.command, group, "outside", hookName, metas, details.key, details.timestamp, res[1:]))
} }
case "roam": case "roam":
if len(msgs) > 0 { if len(msgs) > 0 {
@ -244,12 +259,13 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai
return msgs return msgs
} }
func makemsg(command, group, detect, hookName string, key string, t time.Time, tail []byte) []byte { func makemsg(command, group, detect, hookName string, metas []FenceMeta, key string, t time.Time, tail []byte) []byte {
var buf []byte var buf []byte
buf = append(append(buf, `{"command":"`...), command...) buf = append(append(buf, `{"command":"`...), command...)
buf = append(append(buf, `","group":"`...), group...) buf = append(append(buf, `","group":"`...), group...)
buf = append(append(buf, `","detect":"`...), detect...) buf = append(append(buf, `","detect":"`...), detect...)
buf = appendJSONString(append(buf, `","hook":`...), hookName) buf = append(buf, '"')
buf = appendHookDetails(buf, hookName, metas)
buf = appendJSONString(append(buf, `,"key":`...), key) buf = appendJSONString(append(buf, `,"key":`...), key)
buf = appendJSONTimeFormat(append(buf, `,"time":`...), t) buf = appendJSONTimeFormat(append(buf, `,"time":`...), t)
buf = append(append(buf, ','), tail...) buf = append(append(buf, ','), tail...)

View File

@ -66,21 +66,36 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetai
} }
endpoints = append(endpoints, url) endpoints = append(endpoints, url)
} }
var commandvs []resp.Value
commandvs := vs var cmdlc string
if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
return "", d, errInvalidNumberOfArguments
}
cmdlc := strings.ToLower(cmd)
var types []string var types []string
switch cmdlc { metaMap := make(map[string]string)
default: for {
return "", d, errInvalidArgument(cmd) commandvs = vs
case "nearby": if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
types = nearbyTypes return "", d, errInvalidNumberOfArguments
case "within", "intersects": }
types = withinOrIntersectsTypes cmdlc = strings.ToLower(cmd)
switch cmdlc {
default:
return "", d, errInvalidArgument(cmd)
case "meta":
var metakey string
var metaval string
if vs, metakey, ok = tokenval(vs); !ok || metakey == "" {
return "", d, errInvalidNumberOfArguments
}
if vs, metaval, ok = tokenval(vs); !ok || metaval == "" {
return "", d, errInvalidNumberOfArguments
}
metaMap[metakey] = metaval
continue
case "nearby":
types = nearbyTypes
case "within", "intersects":
types = withinOrIntersectsTypes
}
break
} }
s, err := c.cmdSearchArgs(cmdlc, vs, types) s, err := c.cmdSearchArgs(cmdlc, vs, types)
if err != nil { if err != nil {
@ -99,6 +114,12 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetai
} }
cmsg.Command = strings.ToLower(cmsg.Values[0].String()) cmsg.Command = strings.ToLower(cmsg.Values[0].String())
metas := make([]FenceMeta, 0, len(metaMap))
for key, val := range metaMap {
metas = append(metas, FenceMeta{key, val})
}
sort.Sort(hookMetaByName(metas))
hook := &Hook{ hook := &Hook{
Key: s.key, Key: s.key,
Name: name, Name: name,
@ -107,6 +128,7 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetai
Message: cmsg, Message: cmsg,
db: c.qdb, db: c.qdb,
epm: c.epc, epm: c.epc,
Metas: metas,
} }
hook.cond = sync.NewCond(&hook.mu) hook.cond = sync.NewCond(&hook.mu)
@ -117,27 +139,15 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetai
} }
if h, ok := c.hooks[name]; ok { if h, ok := c.hooks[name]; ok {
// lets see if the previous hook matches the new hook if h.Equals(hook) {
if h.Key == hook.Key && h.Name == hook.Name { // it was a match so we do nothing. But let's signal just
if len(h.Endpoints) == len(hook.Endpoints) { // for good measure.
match := true h.Signal()
for i, endpoint := range h.Endpoints { switch msg.OutputType {
if endpoint != hook.Endpoints[i] { case server.JSON:
match = false return server.OKMessage(msg, start), d, nil
break case server.RESP:
} return ":0\r\n", d, nil
}
if match && resp.ArrayValue(h.Message.Values).Equals(resp.ArrayValue(hook.Message.Values)) {
// it was a match so we do nothing. But let's signal just
// for good measure.
h.Signal()
switch msg.OutputType {
case server.JSON:
return server.OKMessage(msg, start), d, nil
case server.RESP:
return ":0\r\n", d, nil
}
}
} }
} }
h.Close() h.Close()
@ -323,6 +333,7 @@ type Hook struct {
Message *server.Message Message *server.Message
Fence *liveFenceSwitches Fence *liveFenceSwitches
ScanWriter *scanWriter ScanWriter *scanWriter
Metas []FenceMeta
db *buntdb.DB db *buntdb.DB
closed bool closed bool
opened bool opened bool
@ -330,6 +341,46 @@ type Hook struct {
epm *endpoint.EndpointManager epm *endpoint.EndpointManager
} }
func (h *Hook) Equals(hook *Hook) bool {
if h.Key != hook.Key ||
h.Name != hook.Name ||
len(h.Endpoints) != len(hook.Endpoints) ||
len(h.Metas) != len(hook.Metas) {
return false
}
for i, endpoint := range h.Endpoints {
if endpoint != hook.Endpoints[i] {
return false
}
}
for i, meta := range h.Metas {
if meta.Name != hook.Metas[i].Name ||
meta.Value != hook.Metas[i].Value {
return false
}
}
return resp.ArrayValue(h.Message.Values).Equals(
resp.ArrayValue(hook.Message.Values))
}
type FenceMeta struct {
Name, Value string
}
type hookMetaByName []FenceMeta
func (arr hookMetaByName) Len() int {
return len(arr)
}
func (arr hookMetaByName) Less(a, b int) bool {
return arr[a].Name < arr[b].Name
}
func (arr hookMetaByName) Swap(a, b int) {
arr[a], arr[b] = arr[b], arr[a]
}
// Open is called when a hook is first created. It calls the manager // Open is called when a hook is first created. It calls the manager
// function in a goroutine // function in a goroutine
func (h *Hook) Open() { func (h *Hook) Open() {

View File

@ -162,7 +162,7 @@ func (c *Controller) goLive(inerr error, conn net.Conn, rd *server.AnyReaderWrit
} }
fence := lb.fence fence := lb.fence
lb.cond.L.Unlock() lb.cond.L.Unlock()
msgs := FenceMatch("", sw, fence, details) msgs := FenceMatch("", sw, fence, nil, details)
for _, msg := range msgs { for _, msg := range msgs {
if err := writeMessage(conn, []byte(msg), true, connType, websocket); err != nil { if err := writeMessage(conn, []byte(msg), true, connType, websocket); err != nil {
return nil // nil return is fine here return nil // nil return is fine here

View File

@ -1104,6 +1104,13 @@
"name": "endpoint", "name": "endpoint",
"type": "string" "type": "string"
}, },
{
"command": "META",
"name": ["name", "value"],
"type": ["string", "string"],
"optional": true,
"multiple": true
},
{ {
"enum": ["NEARBY", "WITHIN", "INTERSECTS"] "enum": ["NEARBY", "WITHIN", "INTERSECTS"]
}, },

View File

@ -1266,6 +1266,13 @@ var commandsJSON = `{
"name": "endpoint", "name": "endpoint",
"type": "string" "type": "string"
}, },
{
"command": "META",
"name": ["name", "value"],
"type": ["string", "string"],
"optional": true,
"multiple": true
},
{ {
"enum": ["NEARBY", "WITHIN", "INTERSECTS"] "enum": ["NEARBY", "WITHIN", "INTERSECTS"]
}, },