recorder/hooks.c
2023-01-02 14:05:26 +01:00

617 lines
14 KiB
C

/*
* OwnTracks Recorder
* Copyright (C) 2015-2023 Jan-Piet Mens <jpmens@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef WITH_LUA
# include <stdarg.h>
# include <stdint.h>
# include "util.h"
# include "udata.h"
# include "hooks.h"
# include <lua.h>
# include <lualib.h>
# include <lauxlib.h>
# include "fences.h"
# include "gcache.h"
# include "json.h"
# include "version.h"
# include "fences.h"
static int otr_log(lua_State *lua);
static int otr_strftime(lua_State *lua);
static int otr_putdb(lua_State *lua);
static int otr_getdb(lua_State *lua);
#ifdef WITH_MQTT
# include <mosquitto.h>
static int otr_publish(lua_State *lua);
static struct mosquitto *MQTTconn = NULL;
#endif
static struct gcache *LuaDB = NULL;
/*
* Invoke the function `name' in the Lua script, which _may_ return
* an integer that we, in turn, return to caller.
*/
static int l_function(lua_State *L, char *name)
{
int rc = 0;
lua_getglobal(L, name);
if (lua_type(L, -1) != LUA_TFUNCTION) {
olog(LOG_ERR, "Cannot invoke Lua function %s: %s\n", name, lua_tostring(L, -1));
rc = 1;
} else {
lua_call(L, 0, 1);
rc = (int)lua_tonumber(L, -1);
lua_pop(L, 1);
}
lua_settop(L, 0);
return (rc);
}
struct luadata *hooks_init(struct udata *ud, char *script)
{
struct luadata *ld;
int rc;
if ((ld = malloc(sizeof(struct luadata))) == NULL)
return (NULL);
ld->script = strdup(script);
// ld->L = lua_open();
ld->L = luaL_newstate();
luaL_openlibs(ld->L);
#ifdef WITH_MQTT
MQTTconn = ud->mosq;
#endif
/*
* Set up a global with some values.
*/
lua_newtable(ld->L);
lua_pushstring(ld->L, VERSION);
lua_setfield(ld->L, -2, "version");
// lua_pushstring(ld->L, "/Users/jpm/Auto/projects/on-github/owntracks/recorder/lua");
// lua_setfield(ld->L, -2, "luapath");
lua_pushcfunction(ld->L, otr_log);
lua_setfield(ld->L, -2, "log");
lua_pushcfunction(ld->L, otr_strftime);
lua_setfield(ld->L, -2, "strftime");
lua_pushcfunction(ld->L, otr_putdb);
lua_setfield(ld->L, -2, "putdb");
lua_pushcfunction(ld->L, otr_getdb);
lua_setfield(ld->L, -2, "getdb");
#ifdef WITH_MQTT
lua_pushcfunction(ld->L, otr_publish);
lua_setfield(ld->L, -2, "publish");
#endif
lua_setglobal(ld->L, "otr");
LuaDB = ud->luadb;
olog(LOG_DEBUG, "initializing Lua hooks from `%s'", script);
/* Load the Lua script */
if (luaL_dofile(ld->L, ld->script)) {
olog(LOG_ERR, "Cannot load Lua from %s: %s", ld->script, lua_tostring(ld->L, -1));
hooks_exit(ld, "failed to load script");
return (NULL);
}
rc = l_function(ld->L, "otr_init");
if (rc != 0) {
/*
* After all this work (sigh), the Script has decided we shouldn't use
* hooks, so unload the whole Lua stuff and NULL back.
*/
hooks_exit(ld, "otr_init() returned non-zero");
ld = NULL;
}
return (ld);
}
void hooks_exit(struct luadata *ld, char *reason)
{
if (ld && ld->script) {
l_function(ld->L, "otr_exit");
olog(LOG_DEBUG, "unloading Lua: %s", reason);
free(ld->script);
lua_close(ld->L);
free(ld);
}
}
static void do_hook(char *hookname, struct udata *ud, char *topic, JsonNode *fullo)
{
struct luadata *ld = ud->luadata;
char *_type = "unknown";
JsonNode *j;
if (!ld || !ld->script)
return;
lua_settop(ld->L, 0);
lua_getglobal(ld->L, hookname);
if (lua_type(ld->L, -1) != LUA_TFUNCTION) {
olog(LOG_NOTICE, "cannot invoke %s in Lua script", hookname);
return;
}
lua_pushstring(ld->L, topic); /* arg1: topic */
if ((j = json_find_member(fullo, "_type")) != NULL) {
if (j->tag == JSON_STRING)
_type = j->string_;
}
lua_pushstring(ld->L, _type); /* arg2: record type */
lua_newtable(ld->L); /* arg3: table */
json_foreach(j, fullo) {
if (j->tag >= JSON_ARRAY)
continue;
lua_pushstring(ld->L, j->key); /* table key */
if (j->tag == JSON_STRING) {
lua_pushstring(ld->L, j->string_);
} else if (j->tag == JSON_NUMBER) {
lua_pushnumber(ld->L, j->number_);
} else if (j->tag == JSON_NULL) {
lua_pushnil(ld->L);
} else if (j->tag == JSON_BOOL) {
lua_pushboolean(ld->L, j->bool_);
}
lua_rawset(ld->L, -3);
}
/* Invoke `hook' function in Lua with our args */
if (lua_pcall(ld->L, 3, 1, 0)) {
olog(LOG_ERR, "Failed to run script: %s", lua_tostring(ld->L, -1));
exit(1);
}
// rc = (int)lua_tonumber(ld->L, -1);
// printf("C: FILTER returns %d\n", rc);
}
static void hooks_hooklet(struct udata *ud, char *topic, JsonNode *fullo)
{
JsonNode *j;
struct luadata *ld = ud->luadata;
char hookname[BUFSIZ];
if (!ud->luascript)
return;
json_foreach(j, fullo) {
snprintf(hookname, sizeof(hookname), "hooklet_%s", j->key);
lua_getglobal(ld->L, hookname);
if (lua_type(ld->L, -1) == LUA_TFUNCTION) {
do_hook(hookname, ud, topic, fullo);
}
}
}
/*
* Invoked from putrec() in storage. If the Lua putrec function is available
* and it returns non-zero, do not putrec.
*/
int hooks_norec(struct udata *ud, char *user, char *device, char *payload)
{
struct luadata *ld = ud->luadata;
int rc;
if (ld == NULL || !ld->script)
return (0);
lua_settop(ld->L, 0);
lua_getglobal(ld->L, "otr_putrec");
if (lua_type(ld->L, -1) != LUA_TFUNCTION) {
return (0);
}
lua_pushstring(ld->L, user);
lua_pushstring(ld->L, device);
lua_pushstring(ld->L, payload);
/* Invoke Lua function with our args */
if (lua_pcall(ld->L, 3, 1, 0)) {
olog(LOG_ERR, "Failed to run putrec in Lua: %s", lua_tostring(ld->L, -1));
exit(1);
}
rc = (int)lua_tonumber(ld->L, -1);
return (rc);
}
/*
* Invoked in http to populate response FIXME
*/
JsonNode *hooks_http(struct udata *ud, char *user, char *device, char *payload)
{
struct luadata *ld = ud->luadata;
char *_type = "unknown";
JsonNode *obj = NULL, *j, *fullo;
debug(ud, "in hooks_http()");
if (ld == NULL || !ld->script)
return (0);
fullo = json_decode(payload);
lua_settop(ld->L, 0);
lua_getglobal(ld->L, "otr_httpobject");
if (lua_type(ld->L, -1) != LUA_TFUNCTION) {
debug(ud, "no otr_httpobject function in Lua file: returning");
return (0);
}
lua_pushstring(ld->L, user); /* arg1 */
lua_pushstring(ld->L, device); /* arg2 */
if ((j = json_find_member(fullo, "_type")) != NULL) {
if (j->tag == JSON_STRING)
_type = j->string_;
}
lua_pushstring(ld->L, _type); /* arg3: record type */
lua_newtable(ld->L); /* arg4: table */
json_foreach(j, fullo) {
if (j->tag >= JSON_ARRAY)
continue;
lua_pushstring(ld->L, j->key); /* table key */
if (j->tag == JSON_STRING) {
lua_pushstring(ld->L, j->string_);
} else if (j->tag == JSON_NUMBER) {
lua_pushnumber(ld->L, j->number_);
} else if (j->tag == JSON_NULL) {
lua_pushnil(ld->L);
} else if (j->tag == JSON_BOOL) {
lua_pushboolean(ld->L, j->bool_);
}
lua_rawset(ld->L, -3);
}
// lua_pushstring(ld->L, payload);
/* Invoke Lua function with our args */
/* return value is a TABLE; all else is ignored */
if (lua_pcall(ld->L, 4, 1, 0)) {
olog(LOG_ERR, "Failed to run hooks_http in Lua: %s", lua_tostring(ld->L, -1));
exit(1);
}
/* Verify we have a table and create a JSON object from it. */
if (lua_istable(ld->L, -1)) {
obj = json_mkobject();
int t = -2;
lua_pushnil(ld->L); /* first key */
while (lua_next(ld->L, t) != 0) {
const char *key, *val;
size_t len;
int type, bf, nil;
lua_Number d;
key = lua_tolstring(ld->L, -2, &len);
type = lua_type(ld->L, -1);
// printf("%s len=%zd vtype=%d\n", key, len, type);
switch (type) {
case LUA_TNUMBER:
d = lua_tonumber(ld->L, -1);
json_append_member(obj, key, json_mknumber(d));
break;
case LUA_TSTRING:
val = lua_tostring(ld->L, -1);
json_append_member(obj, key, json_mkstring(val));
break;
case LUA_TNIL:
nil = lua_isnil(ld->L, -1);
if (nil)
json_append_member(obj, key, json_mknull());
break;
case LUA_TBOOLEAN:
bf = lua_toboolean(ld->L, -1);
json_append_member(obj, key, json_mkbool(bf));
break;
default:
/* unsupported */
break;
}
lua_pop(ld->L, 1);
}
}
lua_settop(ld->L, 0);
return (obj);
}
void hooks_hook(struct udata *ud, char *topic, JsonNode *fullo)
{
do_hook("otr_hook", ud, topic, fullo);
hooks_hooklet(ud, topic, fullo);
}
/*
* This hook is invoked through fences.c when we determine that a movement
* into or out of a geofence has caused a transition.
* json is the original JSON we received enhanced with stuff from recorder.
*/
void hooks_transition(struct udata *ud, char *user, char *device, int event, char *desc, double wplat, double wplon, double lat, double lon, char *topic, JsonNode *json, long meters)
{
JsonNode *j;
if ((j = json_find_member(json, "_type")) != NULL) {
json_delete(j);
}
json_append_member(json, "_type", json_mkstring("transition"));
json_append_member(json, "event",
event == ENTER ? json_mkstring("enter") : json_mkstring("leave"));
json_append_member(json, "desc", json_mkstring(desc));
json_append_member(json, "wplat", json_mknumber(wplat));
json_append_member(json, "wplon", json_mknumber(wplon));
json_append_member(json, "dist", json_mknumber(meters));
olog(LOG_DEBUG, "**** Lua hook for %s %s\n",
event == ENTER ? "ENTER" : "LEAVE", desc);
do_hook("otr_transition", ud, topic, json);
}
/*
* If the Lua function otr_revgeo() is defined, invoke that to obtain a result
* of reverse geocoding. The function name proper (otr_revgeo) is contained in
* `luafunc'
*/
JsonNode *hook_revgeo(struct udata *ud, char *luafunc, char *topic, char *user, char *device, double lat, double lon)
{
struct luadata *ld = ud->luadata;
JsonNode *obj = NULL;
debug(ud, "in hook_revgeo()");
if (ld == NULL || !ld->script)
return (0);
lua_settop(ld->L, 0);
lua_getglobal(ld->L, luafunc);
if (lua_type(ld->L, -1) != LUA_TFUNCTION) {
debug(ud, "no %s function in Lua file: returning", luafunc);
return (0);
}
lua_pushstring(ld->L, topic); /* arg1 */
lua_pushstring(ld->L, user); /* arg2 */
lua_pushstring(ld->L, device); /* arg3 */
lua_pushnumber(ld->L, lat); /* arg4 */
lua_pushnumber(ld->L, lon); /* arg5 */
/* Invoke Lua function with our args */
/* return value is a TABLE; all else is ignored */
if (lua_pcall(ld->L, 5, 1, 0)) {
olog(LOG_ERR, "Failed to run hook_revgeo in Lua: %s", lua_tostring(ld->L, -1));
exit(1);
}
/* Verify we have a table and create a JSON object from it. */
if (lua_istable(ld->L, -1)) {
obj = json_mkobject();
int t = -2;
lua_pushnil(ld->L); /* first key */
while (lua_next(ld->L, t) != 0) {
const char *key, *val;
size_t len;
int type, bf, nil;
lua_Number d;
key = lua_tolstring(ld->L, -2, &len);
type = lua_type(ld->L, -1);
// printf("%s len=%zd vtype=%d\n", key, len, type);
switch (type) {
case LUA_TNUMBER:
d = lua_tonumber(ld->L, -1);
json_append_member(obj, key, json_mknumber(d));
break;
case LUA_TSTRING:
val = lua_tostring(ld->L, -1);
json_append_member(obj, key, json_mkstring(val));
break;
case LUA_TNIL:
nil = lua_isnil(ld->L, -1);
if (nil)
json_append_member(obj, key, json_mknull());
break;
case LUA_TBOOLEAN:
bf = lua_toboolean(ld->L, -1);
json_append_member(obj, key, json_mkbool(bf));
break;
default:
/* unsupported */
break;
}
lua_pop(ld->L, 1);
}
}
lua_settop(ld->L, 0);
return (obj);
}
/*
* --- Here come the functions we provide to Lua scripts.
*/
static int otr_log(lua_State *lua)
{
const char *str;
if (lua_gettop(lua) >= 1) {
str = lua_tostring(lua, 1);
olog(LOG_INFO, "%s", str);
lua_pop(lua, 1);
}
return 0;
}
/*
* otr.strftime(format, seconds)
* Perform a strtime(3) for Lua with the specified format and
* seconds, and return the string result to Lua. As a special
* case, if `seconds' is negative, use current time.
*/
static int otr_strftime(lua_State *lua)
{
const char *fmt;
time_t secs;
struct tm *tm;
char buf[BUFSIZ];
if (lua_gettop(lua) >= 1) {
fmt = lua_tostring(lua, 1);
if ((secs = (time_t)lua_tonumber(lua, 2)) < 1)
secs = time(0);
if ((tm = gmtime(&secs)) != NULL) {
strftime(buf, sizeof(buf), fmt, tm);
lua_pushstring(lua, buf);
return (1);
}
}
return (0);
}
/*
* Requires two string arguments: key, value
* These are written into the named LMDB database
* called `luadb'.
*/
static int otr_putdb(lua_State *lua)
{
const char *key, *value;
int rc = 0;
if (lua_gettop(lua) >= 1) {
key = lua_tostring(lua, 1);
value = lua_tostring(lua, 2);
rc = gcache_put(LuaDB, (char *)key, (char *)value);
// olog(LOG_DEBUG, "LUA_PUT (%s, %s) == %d\n", key, value, rc);
}
return (rc);
}
static int otr_getdb(lua_State *lua)
{
char buf[BUFSIZ];
const char *key;
int rc = 0, blen;
if (lua_gettop(lua) >= 1) {
key = lua_tostring(lua, 1);
blen = gcache_get(LuaDB, (char *)key, buf, sizeof(buf));
if (blen < 0)
memset(buf, 0, sizeof(buf));
// printf("K=[%s], blen=%d\n", key, blen);
lua_pushstring(lua, buf);
rc = 1;
}
return (rc);
}
#ifdef WITH_MQTT
/*
* Requires two string arguments: topic, payload
* and two numeric args: qos and retain
* Will be published via MQTT to the Recorder's
* open connection.
*/
int otr_publish(lua_State *lua)
{
const char *topic, *payload;
int qos = 0, retain = 0;
int rc = 0;
if (lua_gettop(lua) >= 1) {
topic = lua_tostring(lua, 1);
payload = lua_tostring(lua, 2);
qos = lua_tonumber(lua, 3);
retain = lua_tonumber(lua, 4);
if (MQTTconn == NULL) {
olog(LOG_WARNING, "otr_publish(%s, %s, %d, %d): NULL MQTT connection\n",
topic, payload, qos, retain);
} else {
rc = mosquitto_publish(MQTTconn, NULL, topic,
strlen(payload), payload, qos, retain);
olog(LOG_DEBUG, "otr_publish(%s, %s, %d, %d) == %d\n", topic, payload, qos, retain, rc);
}
}
return (rc);
}
#endif
#endif /* WITH_LUA */