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
)
// 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
// $ ./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 {
for _, hook := range hm {
// 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 {
// append each msg to the big list
hmsgs = append(hmsgs, msgs...)

View File

@ -1,7 +1,6 @@
package controller
import (
"fmt"
"math"
"strconv"
"time"
@ -13,13 +12,8 @@ import (
)
// FenceMatch executes a fence match returns back json messages for fence detection.
func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, details *commandDetailsT) [][]byte {
overall := time.Now()
defer func() {
return
fmt.Printf(">> %v\n", time.Since(overall))
}()
msgs := fenceMatch(hookName, sw, fence, details)
func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas []FenceMeta, details *commandDetailsT) [][]byte {
msgs := fenceMatch(hookName, sw, fence, metas, details)
if len(fence.accept) == 0 {
return msgs
}
@ -42,10 +36,31 @@ func jsonTimeFormat(t time.Time) string {
b = appendJSONTimeFormat(b, t)
return string(b)
}
func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, details *commandDetailsT) [][]byte {
func appendHookDetails(b []byte, hookName string, metas []FenceMeta) []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" {
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] == '*') {
match, _ := glob.Match(fence.glob, details.id)
@ -65,7 +80,7 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai
}
}
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
@ -174,18 +189,18 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai
msgs := make([][]byte, 0, 4)
if fence.detect == nil || fence.detect[detect] {
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)
}
switch detect {
case "enter":
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":
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":
if len(msgs) > 0 {
@ -244,12 +259,13 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai
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
buf = append(append(buf, `{"command":"`...), command...)
buf = append(append(buf, `","group":"`...), group...)
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 = appendJSONTimeFormat(append(buf, `,"time":`...), t)
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)
}
commandvs := vs
if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
return "", d, errInvalidNumberOfArguments
}
cmdlc := strings.ToLower(cmd)
var commandvs []resp.Value
var cmdlc string
var types []string
switch cmdlc {
default:
return "", d, errInvalidArgument(cmd)
case "nearby":
types = nearbyTypes
case "within", "intersects":
types = withinOrIntersectsTypes
metaMap := make(map[string]string)
for {
commandvs = vs
if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
return "", d, errInvalidNumberOfArguments
}
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)
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())
metas := make([]FenceMeta, 0, len(metaMap))
for key, val := range metaMap {
metas = append(metas, FenceMeta{key, val})
}
sort.Sort(hookMetaByName(metas))
hook := &Hook{
Key: s.key,
Name: name,
@ -107,6 +128,7 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetai
Message: cmsg,
db: c.qdb,
epm: c.epc,
Metas: metas,
}
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 {
// lets see if the previous hook matches the new hook
if h.Key == hook.Key && h.Name == hook.Name {
if len(h.Endpoints) == len(hook.Endpoints) {
match := true
for i, endpoint := range h.Endpoints {
if endpoint != hook.Endpoints[i] {
match = false
break
}
}
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
}
}
if h.Equals(hook) {
// 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()
@ -323,6 +333,7 @@ type Hook struct {
Message *server.Message
Fence *liveFenceSwitches
ScanWriter *scanWriter
Metas []FenceMeta
db *buntdb.DB
closed bool
opened bool
@ -330,6 +341,46 @@ type Hook struct {
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
// function in a goroutine
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
lb.cond.L.Unlock()
msgs := FenceMatch("", sw, fence, details)
msgs := FenceMatch("", sw, fence, nil, details)
for _, msg := range msgs {
if err := writeMessage(conn, []byte(msg), true, connType, websocket); err != nil {
return nil // nil return is fine here

View File

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

View File

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