From bafb1823b3fa5bfd086cbdf0b200ebc1e5ab6ca9 Mon Sep 17 00:00:00 2001 From: Josh Baker Date: Thu, 29 Dec 2016 08:50:54 -0700 Subject: [PATCH] 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 --- cmd/tile38-server/main.go | 2 +- controller/aof.go | 2 +- controller/fence.go | 50 ++++++++++------ controller/hooks.go | 121 +++++++++++++++++++++++++++----------- controller/live.go | 2 +- core/commands.json | 7 +++ core/commands_gen.go | 7 +++ 7 files changed, 136 insertions(+), 55 deletions(-) diff --git a/cmd/tile38-server/main.go b/cmd/tile38-server/main.go index 498a1403..f7d8f26f 100644 --- a/cmd/tile38-server/main.go +++ b/cmd/tile38-server/main.go @@ -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 // diff --git a/controller/aof.go b/controller/aof.go index 33d21f0e..f17917a7 100644 --- a/controller/aof.go +++ b/controller/aof.go @@ -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...) diff --git a/controller/fence.go b/controller/fence.go index b2b5cb4d..e9c3f795 100644 --- a/controller/fence.go +++ b/controller/fence.go @@ -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...) diff --git a/controller/hooks.go b/controller/hooks.go index 403b600e..d259a40b 100644 --- a/controller/hooks.go +++ b/controller/hooks.go @@ -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() { diff --git a/controller/live.go b/controller/live.go index 102c9ac5..8c500905 100644 --- a/controller/live.go +++ b/controller/live.go @@ -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 diff --git a/core/commands.json b/core/commands.json index f622252f..b9b08300 100644 --- a/core/commands.json +++ b/core/commands.json @@ -1104,6 +1104,13 @@ "name": "endpoint", "type": "string" }, + { + "command": "META", + "name": ["name", "value"], + "type": ["string", "string"], + "optional": true, + "multiple": true + }, { "enum": ["NEARBY", "WITHIN", "INTERSECTS"] }, diff --git a/core/commands_gen.go b/core/commands_gen.go index 0ac996f0..d0e07d15 100644 --- a/core/commands_gen.go +++ b/core/commands_gen.go @@ -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"] },