// Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 package re import ( "context" "encoding/json" "fmt" "log/slog" pkglog "github.com/absmach/magistrala/pkg/logger" "github.com/absmach/supermq/pkg/errors" "github.com/absmach/supermq/pkg/messaging" "github.com/vadv/gopher-lua-libs/argparse" "github.com/vadv/gopher-lua-libs/base64" bit "github.com/vadv/gopher-lua-libs/bit" "github.com/vadv/gopher-lua-libs/crypto" "github.com/vadv/gopher-lua-libs/db" "github.com/vadv/gopher-lua-libs/filepath" client "github.com/vadv/gopher-lua-libs/http/client" "github.com/vadv/gopher-lua-libs/ioutil" luajson "github.com/vadv/gopher-lua-libs/json" "github.com/vadv/gopher-lua-libs/regexp" "github.com/vadv/gopher-lua-libs/storage" "github.com/vadv/gopher-lua-libs/strings" luatime "github.com/vadv/gopher-lua-libs/time" "github.com/vadv/gopher-lua-libs/yaml" lua "github.com/yuin/gopher-lua" ) const payloadKey = "payload" func (re *re) processLua(ctx context.Context, details []slog.Attr, r Rule, msg *messaging.Message) pkglog.RunInfo { l := lua.NewState() defer l.Close() preload(l) message := prepareMsg(l, msg) // Set the message object as a Lua global variable. l.SetGlobal("message", message) if err := l.DoString(r.Logic.Value); err != nil { return pkglog.RunInfo{Level: slog.LevelError, Message: fmt.Sprintf("failed to run rule logic: %s", err), Details: details} } // Get the last result. result := l.Get(-1) if result == lua.LNil { return pkglog.RunInfo{Level: slog.LevelWarn, Message: "rule with nil script result", Details: details} } // Converting Lua is an expensive operation, so // don't do it if there are no outputs. if len(r.Outputs) == 0 { return pkglog.RunInfo{Level: slog.LevelWarn, Message: "rule with no outputs", Details: details} } var err error res := convertLua(result) for _, o := range r.Outputs { // If value is false, don't run the follow-up. if v, ok := res.(bool); ok && !v { return pkglog.RunInfo{Level: slog.LevelInfo, Message: "logic returned false", Details: details} } if e := re.handleOutput(ctx, o, r, msg, res); e != nil { err = errors.Wrap(e, err) } } ret := pkglog.RunInfo{Level: slog.LevelInfo, Message: "rule processed successfully", Details: details} if err != nil { ret.Level = slog.LevelError ret.Message = fmt.Sprintf("failed to handle rule output: %s", err) } return ret } func preload(l *lua.LState) { db.Preload(l) ioutil.Preload(l) luajson.Preload(l) yaml.Preload(l) crypto.Preload(l) regexp.Preload(l) luatime.Preload(l) storage.Preload(l) base64.Preload(l) argparse.Preload(l) strings.Preload(l) filepath.Preload(l) client.Preload(l) bit.Preload(l) } func prepareMsg(l *lua.LState, msg *messaging.Message) lua.LValue { message := l.NewTable() message.RawSetString("domain", lua.LString(msg.Domain)) message.RawSetString("channel", lua.LString(msg.Channel)) message.RawSetString("subtopic", lua.LString(msg.Subtopic)) message.RawSetString("publisher", lua.LString(msg.Publisher)) message.RawSetString("protocol", lua.LString(msg.Protocol)) message.RawSetString("created", lua.LNumber(msg.Created)) var payload any if err := json.Unmarshal(msg.GetPayload(), &payload); err != nil { pld := l.NewTable() // If message is not JSON, set binary payload and exit. for i, b := range msg.Payload { // Lua tables are 1-indexed. pld.Insert(i+1, lua.LNumber(b)) } message.RawSetString(payloadKey, pld) return message } // Payload is JSON, set the correct value. message.RawSetString(payloadKey, traverseJson(l, payload)) return message } func traverseJson(l *lua.LState, value any) lua.LValue { switch val := value.(type) { case string: return lua.LString(val) case float64: return lua.LNumber(val) case int: return lua.LNumber(float64(val)) case json.Number: if num, err := val.Float64(); err != nil { return lua.LNumber(num) } return lua.LNil case bool: return lua.LBool(val) case []any: t := l.NewTable() for i, j := range val { t.RawSetInt(i+1, traverseJson(l, j)) } return t case map[string]any: t := l.NewTable() for k, v := range val { t.RawSetString(k, traverseJson(l, v)) } return t default: return lua.LNil } } func convertLua(lv lua.LValue) any { switch v := lv.(type) { case *lua.LTable: isArray := true v.ForEach(func(key, value lua.LValue) { if key.Type() != lua.LTNumber { isArray = false } }) if isArray { arr := []any{} v.ForEach(func(key, value lua.LValue) { arr = append(arr, convertLua(value)) }) return arr } obj := map[string]any{} v.ForEach(func(key, value lua.LValue) { obj[key.String()] = convertLua(value) }) return obj case lua.LString: return string(v) case lua.LNumber: return float64(v) case lua.LBool: return bool(v) case *lua.LNilType: return nil default: return v.String() } }