/*  $Id: python.c,v 1.23.2.1 2004/01/25 22:22:14 rra Exp $
**
**  python.c: Embed Python in the style of innd's TCL and Perl stuff.
** 
**  Written by G.J. Andruk <meowing@banet.net> patterned after
**  TCL/Perl work by Bob Heiney and Christophe Wolfhugel and a whole
**  bunch of other people mentioned in the docs and sources for the
**  other filters.
**
**  The astute reader may notice the commission of blatant atrocities
**  against Python's OO model here.  Don't tell Guido.
*/

#include "config.h"
#include "clibrary.h"

#include "inn/innconf.h"
#include "innd.h"


#if defined(DO_PYTHON)

#include "Python.h"


bool		PythonFilterActive;
char		*filterPath;	/* this gets set in art.c */
PyObject	*PYFilterObject = NULL;
PyObject	*PYFilterModule = NULL;

/* article filter bits and pieces */
PyObject	*PYheaders = NULL;
PyObject	**PYheaditem;
PyObject	**PYheadkey;
PyObject	*PYpathkey, *PYlineskey, *PYbodykey;

/* external functions */
PyObject	*msgid_method = NULL;
PyObject	*art_method = NULL;
PyObject	*mode_method = NULL;
PyObject	*pre_reload_method = NULL;
PyObject	*close_method = NULL;



/*
**  Turn filtering on or off.
*/
void
PYfilter(value)
    bool value;
{
    PythonFilterActive = value;
    syslog(L_NOTICE, "%s Python filtering %s", LogName,
	   PythonFilterActive ? "enabled" : "disabled");
}



/*
**  Front end for PYfilter()
*/
const char *
PYcontrol(char **av)
{
    char        *p;
    extern bool PythonFilterActive;

    switch (av[0][0]) {
    default:
        return "1 Bad flag";
    case 'y':
        if (PythonFilterActive)
            return "1 Python filter already enabled";
        else if (PYFilterObject == NULL)
            return "1 Python filter not defined" ;
        PYfilter(true);
        break;
    case 'n':
        if (!PythonFilterActive)
            return "1 Python filter already disabled";
        PYfilter(false);
        break;
    }
    return NULL;
}




/*
**  Reject articles we don't like.
*/
char *
PYartfilter(const ARTDATA *data, char *artBody, long artLen, int lines)
{
    const ARTHEADER *hp;
    const HDRCONTENT *hc = data->HdrContent;
    int		hdrnum;
    int		i;
    char	*p, save;
    static char buf[256];
    PyObject	*result;

    if (!PythonFilterActive || PYFilterObject == NULL || art_method == NULL)
	return NULL;

    /* Add headers to the dictionary... */
    hdrnum = 0;
    for (i = 0 ; i < MAX_ARTHEADER ; i++) {
	if (HDR_FOUND(i)) {
	    hp = &ARTheaders[i];
	    PYheaditem[hdrnum] = PyBuffer_FromMemory(HDR(i), HDR_LEN(i));
	} else
	    PYheaditem[hdrnum] = Py_None;
	PyDict_SetItem(PYheaders, PYheadkey[hdrnum], PYheaditem[hdrnum]);
	hdrnum++;
    }

    /* ...then the body... */
    if (artLen && artBody != NULL)
        PYheaditem[hdrnum] = PyBuffer_FromMemory(artBody, --artLen);
    else
        PYheaditem[hdrnum] = Py_None;
    PyDict_SetItem(PYheaders, PYbodykey, PYheaditem[hdrnum++]);

    /* ...and finally, the line count. */
    PYheaditem[hdrnum] = PyInt_FromLong((long) lines);
    PyDict_SetItem(PYheaders, PYlineskey, PYheaditem[hdrnum++]);

    /* Now see if the filter likes it. */
    result = PyObject_CallFunction(art_method, "O", PYheaders);
    if ((result != NULL) && PyObject_IsTrue(result))
	strlcpy(buf, PyString_AS_STRING(result), sizeof(buf));
    else
	*buf = '\0';
    Py_XDECREF(result);

    /* Clean up after ourselves */
    PyDict_Clear(PYheaders);
    for (i = 0; i < hdrnum; i++)
	if (PYheaditem[i] != Py_None)
	    Py_DECREF(PYheaditem[i]);

    if (*buf != '\0') 
	return buf;
    return NULL;
}



/*
**  Refuse message IDs offered thru CHECK or IHAVE that we don't like.
*/
char *
PYmidfilter(messageID, msglen)
    char *messageID;
    int msglen;
{
    static char		buf[256];
    PyObject		*result;

    if (!PythonFilterActive || PYFilterObject == NULL || msgid_method == NULL)
	return NULL;

    result = PyObject_CallFunction(msgid_method, "s#", messageID, msglen);
    if ((result != NULL) && PyObject_IsTrue(result))
	strlcpy(buf, PyString_AS_STRING(result), sizeof(buf));
    else
	*buf = '\0';
    Py_XDECREF(result);

    if (*buf != '\0') 
	return buf;
    return NULL;
}



/*
**  Tell the external module about innd's state.
*/
void
PYmode(Mode, NewMode, reason)
    OPERATINGMODE Mode, NewMode;
    char *reason;
{
    PyObject	*result;
    char	oldmode[10], newmode[10];

    if (!PythonFilterActive || PYFilterObject == NULL || mode_method == NULL)
	return;

    switch (Mode) {
    default:		strlcpy(oldmode, "unknown", 10);	break;
    case OMrunning:	strlcpy(oldmode, "running", 10);	break;
    case OMpaused:	strlcpy(oldmode, "paused", 10);		break;
    case OMthrottled:	strlcpy(oldmode, "throttled", 10);	break;
    }

    switch (NewMode) {
    default:		strlcpy(newmode, "unknown", 10);	break;
    case OMrunning:	strlcpy(newmode, "running", 10);	break;
    case OMpaused:	strlcpy(newmode, "paused", 10);		break;
    case OMthrottled:	strlcpy(newmode, "throttled", 10);	break;
    }

    result = PyObject_CallFunction(mode_method, "sss",
				   oldmode, newmode, reason);
    Py_DECREF(result);
}



/*
**  Called by the external module so it can register itself with innd.
*/
static PyObject *
PY_set_filter_hook(dummy, args)
    PyObject *dummy, *args;
{
    PyObject	*result = NULL;
    PyObject	*temp;

    if (PyArg_ParseTuple(args, "O:set_filter_hook", &temp)) {
	Py_XINCREF(temp);
	Py_XDECREF(PYFilterObject);
	PYFilterObject = temp;
	Py_INCREF(Py_None);
	result = Py_None;
    }
    return result;
}



/*
**  Allow external module to ask innd if an ID is in history.
*/
static PyObject *
PY_havehist(self, args)
    PyObject *self, *args;
{
    char	*msgid;
    int		msgidlen;

    if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
	return NULL;

    if (HIScheck(History, msgid))
	return PyInt_FromLong(1);
    return PyInt_FromLong(0);
}



/*
**  Allow external module to locally delete an article.
*/
static PyObject *
PY_cancel(self, args)
    PyObject *self, *args;
{
    char	*msgid;
    int		msgidlen;
    char	*parambuf[2];

    if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
	return NULL;

    parambuf[0]= msgid;
    parambuf[1]= 0;

    if (!CCcancel(parambuf))
	return PyInt_FromLong(1);
    return PyInt_FromLong(0);
}



/*
**  Stuff an ID into history so that it will be refused later.
*/
static PyObject *
PY_addhist(self, args)
    PyObject *self, *args;
{
    char	*msgid;
    int		msgidlen;
    char	*articlepaths = "";
    char	tbuff[12];
    char	*parambuf[6];

    if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
	return NULL;

    snprintf(tbuff, sizeof(tbuff), "%d", time(NULL));

    parambuf[0] = msgid;
    parambuf[1] = parambuf[2] = parambuf[3] = tbuff;
    parambuf[4] = articlepaths;
    parambuf[5] = 0;

    if (!CCaddhist(parambuf))
	return PyInt_FromLong(1);
    return PyInt_FromLong(0);
}



/*
**  Get a newsgroup's status flag (j, m, n, x, y, =other.group)
*/
static PyObject *
PY_newsgroup(self, args)
    PyObject *self, *args;
{
    char	*newsgroup;
    int		nglen;
    NEWSGROUP	*ngp;
    char	*end;
    char	*rest;
    int		size;

    if (!PyArg_ParseTuple(args, "s#", &newsgroup, &nglen))
	return NULL;

    ngp = NGfind(newsgroup);
    if (ngp == NULL)
	return PyString_FromStringAndSize(NULL, 0);

    /* ngp->Rest is newline-terminated; find the end. */
    end = strchr(ngp->Rest, '\n');
    if (end == NULL)
	size = strlen(ngp->Rest);
    else
	size = end - ngp->Rest;

    /* If an alias is longer than this, active is probably broken. */
    if (size > MAXHEADERSIZE) {
	syslog(L_ERROR, "too-long flag field in active for %s", newsgroup);
	size = MAXHEADERSIZE;
    }

    return PyString_FromStringAndSize(ngp->Rest, size);
}



/*
**  Return an article header to the external module as a string.  We
**  don't use a buffer object here because that would make it harder,
**  for example, to compare two on-spool articles.
*/
static PyObject *
PY_head(self, args)
    PyObject *self, *args;
{
    char	*msgid;
    int		msgidlen;
    char	*p;
    TOKEN	token;
    ARTHANDLE	*art;
    PyObject	*header;
    int		headerlen;

    if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
	return NULL;

    if (! HISlookup(History, msgid, NULL, NULL, NULL, &token))
	return Py_BuildValue("s", "");	
    if ((art = SMretrieve(token, RETR_HEAD)) == NULL)
	return Py_BuildValue("s", "");	
    p = FromWireFmt(art->data, art->len, &headerlen);
    SMfreearticle(art);
    header = PyString_FromStringAndSize(p, headerlen);
    free(p);

    return header;
}



/*
**  Return a whole article to the external module as a string.
*/
static PyObject *
PY_article(self, args)
    PyObject *self, *args;
{
    char	*msgid;
    int		msgidlen;
    char	*p;
    TOKEN	token;
    ARTHANDLE	*arth;
    PyObject	*art;
    int		artlen;

    if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
	return NULL;

    if (! HISlookup(History, msgid, NULL, NULL, NULL, &token))
	return Py_BuildValue("s", "");
    if ((arth = SMretrieve(token, RETR_ALL)) == NULL)
	return Py_BuildValue("s", "");	
    p = FromWireFmt(arth->data, arth->len, &artlen);
    SMfreearticle(arth);
    art = PyString_FromStringAndSize(p, artlen);
    free(p);

    return art;
}



/*
**  Python's syslog module isn't compiled in by default.  It's easier
**  to do it this way, and the switch block looks pretty in a color
**  editor).
*/
static PyObject *
PY_syslog(self, args)
    PyObject *self, *args;
{
    char	*loglevel;
    int		levellen;
    char	*logmsg;
    int		msglen;
    int		priority;

    if (!PyArg_ParseTuple(args, "s#s#",
			  &loglevel, &levellen, &logmsg, &msglen))
	return NULL;

    switch (*loglevel) {
    default:		priority = LOG_NOTICE ;
    case 'd': case 'D': priority = LOG_DEBUG ;		break;
    case 'i': case 'I': priority = LOG_INFO ;		break;
    case 'n': case 'N': priority = LOG_NOTICE ;		break;
    case 'w': case 'W': priority = LOG_WARNING ;	break;
    case 'e': case 'E': priority = LOG_ERR ;		break;
    case 'c': case 'C': priority = LOG_CRIT ;		break;
    case 'a': case 'A': priority = LOG_ALERT ;		break;
    }

    syslog(priority, "python: %s", logmsg);

    Py_INCREF(Py_None);
    return Py_None;
}



/*
**  Compute a hash digest for a string.
*/
static PyObject *
PY_hashstring(self, args)
    PyObject *self, *args;
{
    char	*instring, *wpos, *p, *q;
    char	*workstring = NULL;
    int		insize, worksize, newsize, i, wasspace;
    int		lines = 0;
    HASH	myhash;

    if (!PyArg_ParseTuple(args, "s#|i", &instring, &insize, &lines))
	return NULL;

    /* If a linecount is provided, munge before hashing. */
    if (lines > 0) {
	worksize = insize;

	/* chop leading whitespace */
	for (p=instring ; worksize>0 && isspace(*p) ; p++) {
	    if (*p == '\n')
		lines--;
	    worksize--;
	}
	wpos = p;

	/* and trailing */
	for (p=&wpos[worksize] ; worksize>0 && isspace(*p) ; p--) {
	    if (*p == '\n')
		lines--;
	    worksize--;
	}

	/* chop last 3 lines if we have >= 5.  From above chop the
	 * last line has no CR so we use 1 less here. */
	if (lines >= 4) {
	    for (i=0, p=wpos+worksize ; i<2 ; p--)
		if (*p == '\n')
		    i++;
	    worksize = p - wpos;
	}

	/* Compress out multiple whitespace in the trimmed string.  We
	 * do a copy because this is probably an original art
	 * buffer. */
	workstring =  memcpy(xmalloc(worksize), wpos, worksize);
	newsize = wasspace = 0;
	p = wpos;
	q = workstring;
	for (i=0 ; i<worksize ; i++) {
	    if (isspace(*p)) {
		if (!wasspace)
		    *q++ = ' ';
		wasspace = 1;
	    }
	    else {
		*q++ = tolower(*p);
		wasspace = 0;
	    }
	    p++;
	}
	worksize = q - workstring;
	myhash = Hash(workstring, worksize);
	free(workstring);
    }
    else
	myhash = Hash(instring, insize);

    return PyString_FromStringAndSize((const char *)&myhash, sizeof(myhash));
}



/*
**  Make the internal INN module's functions visible to Python.
*/
static PyMethodDef INNPyMethods[] = {
    {"set_filter_hook", PY_set_filter_hook,	METH_VARARGS},
    {"havehist",	PY_havehist,		METH_VARARGS},
    {"addhist",		PY_addhist,		METH_VARARGS},
    {"cancel",		PY_cancel,		METH_VARARGS},
    {"newsgroup",	PY_newsgroup,		METH_VARARGS},
    {"head",		PY_head,		METH_VARARGS},
    {"article",		PY_article,		METH_VARARGS},
    {"syslog",		PY_syslog,		METH_VARARGS},
    {"hashstring",	PY_hashstring,		METH_VARARGS},
    {NULL,		NULL}
};



/*
**  This runs when innd shuts down.
*/
void
PYclose(void)
{
    PyObject	*result;

    if (close_method != NULL) {
	result = PyObject_CallFunction(close_method, NULL);
	Py_XDECREF(result);
    }
}



/*
**  Check that a method exists and is callable.	 Set a pointer to
**  the corresponding PyObject, or NULL if not found.
*/
void
PYdefonemethod(methptr, methname)
    PyObject	**methptr;
    char	*methname;
{
    Py_XDECREF(*methptr);
    *methptr = PyObject_GetAttrString(PYFilterObject, methname);
    if (*methptr == NULL)
	syslog(L_NOTICE, "python method %s not found", methname);
    else if (PyCallable_Check(*methptr) == 0) {
	syslog(L_ERROR, "python object %s found but not a function", methname);
	Py_DECREF(*methptr);
	*methptr = NULL;
    }
}



/*
**  Look up the filter methods, so we will know what's available when
**  innd wants to call them.
*/
void
PYdefmethods(void)
{
    PYdefonemethod(&msgid_method, "filter_messageid");
    PYdefonemethod(&art_method, "filter_art");
    PYdefonemethod(&mode_method, "filter_mode");
    PYdefonemethod(&pre_reload_method, "filter_before_reload");
    PYdefonemethod(&close_method, "filter_close");
}



/*
**  Used by `ctlinnd reload filter.python'
*/
int
PYreadfilter(void)
{
    PyObject	*newmodule = NULL;
    PyObject	*result;

    if (!Py_IsInitialized()) {
	syslog(L_ERROR, "python is not initialized");
	return 0;
    }

    /* If there is a filter running, let it clean up first. */
    if (pre_reload_method != NULL) {
	result = PyObject_CallFunction(pre_reload_method, NULL);
	Py_XDECREF(result);
    }

    if ((newmodule = PyImport_ReloadModule(PYFilterModule)) == NULL) {
	syslog(L_ERROR, "cant reload python filter module");
	PYfilter(false);
	return 0;
    }
    Py_DECREF(PYFilterModule);
    PYFilterModule = newmodule;
    if (PYFilterObject == NULL) {
	syslog(L_ERROR, "python reload error, filter object not defined");
	PYfilter(false);
	return 0;
    }
    PYfilter(true);
    PYdefmethods();
    return 1;
}



/*
**  Called when innd first starts -- this gets the filters hooked in.
*/
void
PYsetup(void)
{
    const ARTHEADER *hp;
    int		hdrindex;
    size_t hdrcount;

    setenv("PYTHONPATH", innconf->pathfilter, 1);
    Py_Initialize();

    /* It makes Python sad when its stdout and stderr are closed. */
    if ((fileno(stdout) == -1) || (fileno(stderr) == -1))
	PyRun_SimpleString
	    ("import sys; sys.stdout=sys.stderr=open('/dev/null', 'a')");

    if (!Py_IsInitialized ()) {
	syslog(L_ERROR, "python interpreter NOT initialized");
	return;
    }
    syslog(L_NOTICE, "python interpreter initialized OK");

    Py_InitModule("INN", INNPyMethods);

    PYFilterModule = PyImport_ImportModule(_PATH_PYTHON_STARTUP_M);
    if (PYFilterModule == NULL)
	syslog(L_ERROR, "failed to import external python module");

    if (PYFilterObject == NULL) {
	syslog(L_ERROR, "python filter object is not defined");
	PYfilter(false);
    } else {
	PYfilter(true);
	PYdefmethods();
	syslog(L_NOTICE, "defined python methods");
    }

    /* Grab space for these so we aren't forever recreating them.  We also
       put the body and the line count into PYheaditem, so it needs to be
       two elements longer than the total number of headers. */
    PYheaders = PyDict_New();
    hdrcount = ARRAY_END(ARTheaders) - ARTheaders;
    PYheaditem = xmalloc((hdrcount + 2) * sizeof(PyObject *));
    PYheadkey = xmalloc(hdrcount * sizeof(PyObject *));

    /* Preallocate keys for the article dictionary */
    for (hp = ARTheaders; hp < ARRAY_END(ARTheaders); hp++)
	PYheadkey[hp - ARTheaders] = PyString_InternFromString(hp->Name);
    PYpathkey = PyString_InternFromString("Path");
    PYlineskey = PyString_InternFromString("__LINES__");
    PYbodykey = PyString_InternFromString("__BODY__");
}

#endif /* defined(DO_PYTHON) */
