Cleaned up the Python module a bit.
[doldaconnect.git] / lib / python / dolmod.c
1 /*
2  *  Dolda Connect - Modular multiuser Direct Connect-style client
3  *  Copyright (C) 2004 Fredrik Tolf <fredrik@dolda2000.com>
4  *  
5  *  This program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2 of the License, or
8  *  (at your option) any later version.
9  *  
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *  
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program; if not, write to the Free Software
17  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 */
19 #include <Python.h>
20 #include <sys/poll.h>
21 #include <doldaconnect/uilib.h>
22 #include <doldaconnect/uimisc.h>
23 #include <doldaconnect/utils.h>
24 #include <wchar.h>
25
26 struct respobj {
27     PyObject_HEAD
28     struct dc_response *resp;
29 };
30
31 static int fd = -1;
32
33 static void resptype_del(struct respobj *self)
34 {
35     dc_freeresp(self->resp);
36     self->ob_type->tp_free((PyObject *)self);
37 }
38
39 static PyObject *resp_getcode(struct respobj *self)
40 {
41     return(Py_BuildValue("i", self->resp->code));
42 }
43
44 static PyObject *resp_gettag(struct respobj *self)
45 {
46     return(Py_BuildValue("i", self->resp->tag));
47 }
48
49 static PyObject *resp_getcmd(struct respobj *self)
50 {
51     return(PyUnicode_FromWideChar(self->resp->cmdname, wcslen(self->resp->cmdname)));
52 }
53
54 static PyObject *resp_extract(struct respobj *self)
55 {
56     int i, o;
57     PyObject *ret, *sl;
58     wchar_t *c;
59     
60     ret = PyList_New(self->resp->numlines);
61     for(i = 0; i < self->resp->numlines; i++) {
62         sl = PyList_New(self->resp->rlines[i].argc);
63         for(o = 0; o < self->resp->rlines[i].argc; o++) {
64             c = self->resp->rlines[i].argv[o];
65             PyList_SetItem(sl, o, PyUnicode_FromWideChar(c, wcslen(c)));
66         }
67         PyList_SetItem(ret, i, sl);
68     }
69     return(ret);
70 }
71
72 static PyObject *resp_intresp(struct respobj *self)
73 {
74     int i;
75     PyObject *ret, *sl;
76     struct dc_intresp *ires;
77     
78     ret = PyList_New(0);
79     self->resp->curline = 0;
80     while((ires = dc_interpret(self->resp)) != NULL) {
81         sl = PyList_New(ires->argc);
82         for(i = 0; i < ires->argc; i++) {
83             switch(ires->argv[i].type) {
84             case 1:
85                 PyList_SetItem(sl, i, PyUnicode_FromWideChar(ires->argv[i].val.str, wcslen(ires->argv[i].val.str)));
86                 break;
87             case 2:
88                 PyList_SetItem(sl, i, PyInt_FromLong(ires->argv[i].val.num));
89                 break;
90             case 3:
91                 PyList_SetItem(sl, i, PyFloat_FromDouble(ires->argv[i].val.flnum));
92                 break;
93             case 4:
94                 PyList_SetItem(sl, i, PyLong_FromLongLong(ires->argv[i].val.lnum));
95                 break;
96             }
97         }
98         dc_freeires(ires);
99         PyList_Append(ret, sl);
100     }
101     return(ret);
102 }
103
104 static PyMethodDef respmethods[] = {
105     {"getcode", (PyCFunction)resp_getcode, METH_NOARGS,
106      "Get the numerical code from the response"},
107     {"gettag", (PyCFunction)resp_gettag, METH_NOARGS,
108      "Get the tag from the response"},
109     {"getcmd", (PyCFunction)resp_getcmd, METH_NOARGS,
110      "Get the command name from the response"},
111     {"extract", (PyCFunction)resp_extract, METH_NOARGS,
112      "Extract the lines and columns from the response"},
113     {"intresp", (PyCFunction)resp_intresp, METH_NOARGS,
114      "Interpret all the lines from the response with native datatypes"},
115     {NULL, NULL, 0, NULL}
116 };
117
118 static PyTypeObject resptype = {
119     PyObject_HEAD_INIT(NULL)
120     .tp_name = "dolcon.Response",
121     .tp_basicsize = sizeof(struct respobj),
122     .tp_flags = Py_TPFLAGS_DEFAULT,
123     .tp_doc = "Dolda Connect response objects",
124     .tp_dealloc = (destructor)resptype_del,
125     .tp_methods = respmethods,
126 };
127
128 static struct respobj *makeresp(struct dc_response *resp)
129 {
130     struct respobj *new;
131     
132     new = (struct respobj *)resptype.tp_alloc(&resptype, 0);
133     new->resp = resp;
134     return(new);
135 }
136
137 static PyObject *mod_connect(PyObject *self, PyObject *args)
138 {
139     char *host;
140     
141     host = NULL;
142     if(!PyArg_ParseTuple(args, "|s", &host))
143         return(NULL);
144     if(fd >= 0)
145         dc_disconnect();
146     if((fd = dc_connect(host)) < 0) {
147         PyErr_SetFromErrno(PyExc_OSError);
148         return(NULL);
149     }
150     return(Py_BuildValue("i", fd));
151 }
152
153 static PyObject *mod_disconnect(PyObject *self, PyObject *args)
154 {
155     if(fd != -1) {
156         dc_disconnect();
157         fd = -1;
158     }
159     Py_RETURN_NONE;
160 }
161
162 static PyObject *mod_connected(PyObject *self, PyObject *args)
163 {
164     if(fd == -1)
165         Py_RETURN_FALSE;
166     else
167         Py_RETURN_TRUE;
168 }
169
170 static PyObject *mod_select(PyObject *self, PyObject *args)
171 {
172     struct pollfd pfd;
173     int timeout, ret;
174     
175     timeout = -1;
176     if(!PyArg_ParseTuple(args, "|i", &timeout))
177         return(NULL);
178     if(fd < 0) {
179         PyErr_SetString(PyExc_RuntimeError, "Not connected");
180         return(NULL);
181     }
182     pfd.fd = fd;
183     pfd.events = POLLIN;
184     if(dc_wantwrite())
185         pfd.events |= POLLOUT;
186     if((ret = poll(&pfd, 1, timeout)) < 0) {
187         if(errno == EINTR)
188             Py_RETURN_FALSE;
189         dc_disconnect();
190         fd = -1;
191         PyErr_SetFromErrno(PyExc_OSError);
192         return(NULL);
193     }
194     if(((pfd.revents & POLLIN) && dc_handleread()) || ((pfd.revents & POLLOUT) && dc_handlewrite())) {
195         fd = -1;
196         if(errno == 0)
197             Py_RETURN_FALSE;
198         PyErr_SetFromErrno(PyExc_OSError);
199         return(NULL);
200     }
201     if(ret > 0)
202         Py_RETURN_TRUE;
203     Py_RETURN_FALSE;
204 }
205
206 static PyObject *mod_getresp(PyObject *self, PyObject *args)
207 {
208     int tag;
209     struct dc_response *resp;
210     
211     tag = -1;
212     if(!PyArg_ParseTuple(args, "|i", &tag))
213         return(NULL);
214     if(tag == -1)
215         resp = dc_getresp();
216     else
217         resp = dc_gettaggedresp(tag);
218     if(resp == NULL)
219         Py_RETURN_NONE;
220     return((PyObject *)makeresp(resp));
221 }
222
223 static int qcmd_cb(struct dc_response *resp)
224 {
225     PyObject *pycb, *args, *ret;
226     
227     pycb = resp->data;
228     args = Py_BuildValue("(N)", makeresp(resp));
229     ret = PyObject_Call(pycb, args, NULL);
230     Py_DECREF(args);
231     Py_DECREF(ret);
232     Py_DECREF(pycb);
233     return(2);
234 }
235
236 static PyObject *mod_qcmd(PyObject *self, PyObject *args, PyObject *kwargs)
237 {
238     int i;
239     wchar_t **toks, *tok, *cmd;
240     size_t tokssize, toksdata, toksize;
241     PyObject *c, *n, *cb, *ret;
242     
243     toks = NULL;
244     tokssize = toksdata = 0;
245     cmd = NULL;
246     ret = NULL;
247     for(i = 0; i < PySequence_Size(args); i++) {
248         if((c = PySequence_GetItem(args, i)) == NULL)
249             goto out;
250         if(!PyUnicode_Check(c)) {
251             n = PyUnicode_FromObject(c);
252             Py_DECREF(c);
253             if((c = n) == NULL)
254                 goto out;
255         }
256         tok = smalloc((toksize = (PyUnicode_GetSize(c) + 1)) * sizeof(*tok));
257         tok[PyUnicode_AsWideChar((PyUnicodeObject *)c, tok, toksize)] = L'\0';
258         Py_DECREF(c);
259         if(cmd == NULL)
260             cmd = tok;
261         else
262             addtobuf(toks, tok);
263     }
264     if(cmd == NULL) {
265         PyErr_SetString(PyExc_TypeError, "qcmd needs at least 1 argument");
266         goto out;
267     }
268     addtobuf(toks, NULL);
269     ret = NULL;
270     if(PyMapping_HasKeyString(kwargs, "cb")) {
271         cb = PyMapping_GetItemString(kwargs, "cb");
272         if(PyCallable_Check(cb)) {
273             ret = PyInt_FromLong(dc_queuecmd(qcmd_cb, cb, cmd, L"%a", toks, NULL));
274         } else {
275             PyErr_SetString(PyExc_TypeError, "Callback must be callable");
276             Py_DECREF(cb);
277         }
278     } else {
279         ret = PyInt_FromLong(dc_queuecmd(NULL, NULL, cmd, L"%a", toks, NULL));
280     }
281
282 out:
283     dc_freewcsarr(toks);
284     if(cmd != NULL)
285         free(cmd);
286     return(ret);
287 }
288
289 static void login_cb(int err, wchar_t *reason, PyObject *cb)
290 {
291     char *errstr;
292     PyObject *args, *pyerr, *pyreason, *ret;
293     
294     switch(err) {
295     case DC_LOGIN_ERR_SUCCESS:
296         errstr = "success";
297         break;
298     case DC_LOGIN_ERR_NOLOGIN:
299         errstr = "nologin";
300         break;
301     case DC_LOGIN_ERR_SERVER:
302         errstr = "server";
303         break;
304     case DC_LOGIN_ERR_USER:
305         errstr = "user";
306         break;
307     case DC_LOGIN_ERR_CONV:
308         errstr = "conv";
309         break;
310     case DC_LOGIN_ERR_AUTHFAIL:
311         errstr = "authfail";
312         break;
313     default:
314         errstr = "unknown";
315         break;
316     }
317     pyerr = PyString_FromString(errstr);
318     if(reason == NULL)
319         Py_INCREF(pyreason = Py_None);
320     else
321         pyreason = PyUnicode_FromWideChar(reason, wcslen(reason));
322     args = PyTuple_Pack(2, pyerr, pyreason);
323     Py_DECREF(pyerr); Py_DECREF(pyreason);
324     ret = PyObject_Call(cb, args, NULL);
325     Py_DECREF(cb);
326     Py_DECREF(args);
327     Py_DECREF(ret);
328 }
329
330 static PyObject *mod_loginasync(PyObject *self, PyObject *args, PyObject *kwargs)
331 {
332     int useauthless;
333     char *username;
334     PyObject *o, *cb, *conv;
335     
336     username = NULL;
337     conv = NULL;
338     useauthless = 1;
339     if(!PyArg_ParseTuple(args, "O|i", &cb, &useauthless))
340         return(NULL);
341     if(!PyCallable_Check(cb)) {
342         PyErr_SetString(PyExc_TypeError, "Callback must be callable");
343         return(NULL);
344     }
345     if(PyMapping_HasKeyString(kwargs, "username")) {
346         o = PyMapping_GetItemString(kwargs, "username");
347         username = PyString_AsString(o);
348         Py_DECREF(o);
349         if(username == NULL)
350             return(NULL);
351     }
352     if(PyMapping_HasKeyString(kwargs, "conv")) {
353         conv = PyMapping_GetItemString(kwargs, "conv");
354         if(!PyCallable_Check(conv)) {
355             PyErr_SetString(PyExc_TypeError, "Conv callback must be callable");
356             return(NULL);
357         }
358         PyErr_SetString(PyExc_NotImplementedError, "Custom conv functions are not yet supported by the Python interface");
359         return(NULL);
360     }
361     Py_INCREF(cb);
362     dc_loginasync(username, useauthless, NULL, (void (*)(int, wchar_t *, void *))login_cb, cb);
363     Py_RETURN_NONE;
364 }
365
366 static PyObject *mod_lexsexpr(PyObject *self, PyObject *args)
367 {
368     PyObject *arg, *se, *ret;
369     wchar_t **arr, **ap, *buf;
370     size_t bufsize;
371     
372     if(!PyArg_ParseTuple(args, "O", &arg))
373         return(NULL);
374     if((se = PyUnicode_FromObject(arg)) == NULL)
375         return(NULL);
376     buf = smalloc((bufsize = (PyUnicode_GetSize(se) + 1)) * sizeof(*buf));
377     buf[PyUnicode_AsWideChar((PyUnicodeObject *)se, buf, bufsize)] = L'\0';
378     arr = dc_lexsexpr(buf);
379     free(buf);
380     Py_DECREF(se);
381     ret = PyList_New(0);
382     if(arr != NULL) {
383         for(ap = arr; *ap; ap++)
384             PyList_Append(ret, PyUnicode_FromWideChar(*ap, wcslen(*ap)));
385         dc_freewcsarr(arr);
386     }
387     return(ret);
388 }
389
390 static PyObject *mod_wantwrite(PyObject *self)
391 {
392     if(dc_wantwrite())
393         Py_RETURN_TRUE;
394     else
395         Py_RETURN_FALSE;
396 }
397
398 static PyObject *mod_checkproto(PyObject *self, PyObject *args)
399 {
400     PyObject *tmp;
401     struct respobj *resp;
402     int version;
403     
404     version = DC_LATEST;
405     if(!PyArg_ParseTuple(args, "O|i", &tmp, &version))
406         return(NULL);
407     if(!PyObject_TypeCheck(tmp, &resptype)) {
408         PyErr_SetString(PyExc_TypeError, "first argument must be a response object");
409         return(NULL);
410     }
411     resp = (struct respobj *)tmp;
412     if(dc_checkprotocol(resp->resp, version))
413         Py_RETURN_FALSE;
414     else
415         Py_RETURN_TRUE;
416 }
417
418 static PyMethodDef methods[] = {
419     {"connect", mod_connect, METH_VARARGS,
420      "Connect to a Dolda Connect server"},
421     {"disconnect", mod_disconnect, METH_VARARGS,
422      "Disconnect from the server"},
423     {"connected", mod_connected, METH_VARARGS,
424      "Return a boolean indicated whether there currently is a connection to a server"},
425     {"select", mod_select, METH_VARARGS,
426      "Handle data from the server connection, optionally blocking until something happens"},
427     {"getresp", mod_getresp, METH_VARARGS,
428      "Get a queued response object"},
429     {"qcmd", (PyCFunction)mod_qcmd, METH_VARARGS | METH_KEYWORDS,
430      "Queue a command to be sent to the server"},
431     {"loginasync", (PyCFunction)mod_loginasync, METH_VARARGS | METH_KEYWORDS,
432      "Perform an asynchronous login procedure"},
433     {"lexsexpr", mod_lexsexpr, METH_VARARGS,
434      "Use a standard algorithm to lex a search expression"},
435     {"wantwrite", (PyCFunction)mod_wantwrite, METH_NOARGS,
436      "Return a boolean indicating whether there is output to be fed to the server"},
437     {"checkproto", (PyCFunction)mod_checkproto, METH_VARARGS,
438      "Check so that the connect stanza returned by the server indicates support for the correct revision of the protocol"},
439     {NULL, NULL, 0, NULL}
440 };
441
442 PyMODINIT_FUNC initdolmod(void)
443 {
444     PyObject *m;
445     
446     if(PyType_Ready(&resptype) < 0)
447         return;
448     m = Py_InitModule("dolmod", methods);
449     Py_INCREF(&resptype);
450     PyModule_AddObject(m, "Response", (PyObject *)&resptype);
451     PyModule_AddObject(m, "latest", Py_BuildValue("i", DC_LATEST));
452     dc_init();
453 }