Create more directories on whichs presence we rely on at runtime.
[citadel.git] / citadel / modules / xmpp / xmpp_presence.c
1 /*
2  * Handle XMPP presence exchanges
3  *
4  * Copyright (c) 2007-2010 by Art Cancro
5  *
6  * This program is open source software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  *
20  */
21
22 #include "sysdep.h"
23 #include <stdlib.h>
24 #include <unistd.h>
25 #include <stdio.h>
26 #include <fcntl.h>
27 #include <signal.h>
28 #include <pwd.h>
29 #include <errno.h>
30 #include <sys/types.h>
31 #include <assert.h>
32
33 #if TIME_WITH_SYS_TIME
34 # include <sys/time.h>
35 # include <time.h>
36 #else
37 # if HAVE_SYS_TIME_H
38 #  include <sys/time.h>
39 # else
40 #  include <time.h>
41 # endif
42 #endif
43
44 #include <sys/wait.h>
45 #include <string.h>
46 #include <limits.h>
47 #include <ctype.h>
48 #include <expat.h>
49 #include <libcitadel.h>
50 #include "citadel.h"
51 #include "server.h"
52 #include "citserver.h"
53 #include "support.h"
54 #include "config.h"
55 #include "internet_addressing.h"
56 #include "md5.h"
57 #include "ctdl_module.h"
58 #include "serv_xmpp.h"
59
60
61
62 /* 
63  * Indicate the presence of another user to the client
64  * (used in several places)
65  */
66 void xmpp_indicate_presence(char *presence_jid)
67 {
68         char xmlbuf[256];
69
70         cprintf("<presence from=\"%s\" ", xmlesc(xmlbuf, presence_jid, sizeof xmlbuf));
71         cprintf("to=\"%s\"></presence>", xmlesc(xmlbuf, XMPP->client_jid, sizeof xmlbuf));
72 }
73
74
75
76 /*
77  * Convenience function to determine whether any given session is 'visible' to any other given session,
78  * and is capable of receiving instant messages from that session.
79  */
80 int xmpp_is_visible(struct CitContext *cptr, struct CitContext *to_whom) {
81         int aide = (to_whom->user.axlevel >= AxAideU);
82
83         if (    (cptr->logged_in)
84                 &&      (((cptr->cs_flags&CS_STEALTH)==0) || (aide))    /* aides see everyone */
85                 &&      (cptr->user.usernum != to_whom->user.usernum)   /* don't show myself */
86                 &&      (cptr->can_receive_im)                          /* IM-capable session */
87         ) {
88                 return(1);
89         }
90         else {
91                 return(0);
92         }
93 }
94
95
96 /* 
97  * Initial dump of the entire wholist
98  */
99 void xmpp_wholist_presence_dump(void)
100 {
101         struct CitContext *cptr = NULL;
102         int nContexts, i;
103         
104         cptr = CtdlGetContextArray(&nContexts);
105         if (!cptr) {
106                 return;
107         }
108
109         for (i=0; i<nContexts; i++) {
110                 if (xmpp_is_visible(&cptr[i], CC)) {
111                         xmpp_indicate_presence(cptr[i].cs_inet_email);
112                 }
113         }
114         free(cptr);
115 }
116
117
118 /*
119  * Function to remove a buddy subscription and delete from the roster
120  * (used in several places)
121  */
122 void xmpp_destroy_buddy(char *presence_jid, int aggressively) {
123         static int unsolicited_id = 1;
124         char xmlbuf1[256];
125         char xmlbuf2[256];
126
127         if (!presence_jid) return;
128         if (!XMPP) return;
129         if (!XMPP->client_jid) return;
130
131         /* Transmit non-presence information */
132         cprintf("<presence type=\"unavailable\" from=\"%s\" to=\"%s\"></presence>",
133                 xmlesc(xmlbuf1, presence_jid, sizeof xmlbuf1),
134                 xmlesc(xmlbuf2, XMPP->client_jid, sizeof xmlbuf2)
135         );
136
137         /*
138          * Setting the "aggressively" flag also sends an "unsubscribed" presence update.
139          * We only ask for this when flushing the client side roster, because if we do it
140          * in the middle of a session when another user logs off, some clients (Jitsi) interpret
141          * it as a rejection of a subscription request.
142          */
143         if (aggressively) {
144                 cprintf("<presence type=\"unsubscribed\" from=\"%s\" to=\"%s\"></presence>",
145                         xmlesc(xmlbuf1, presence_jid, sizeof xmlbuf1),
146                         xmlesc(xmlbuf2, XMPP->client_jid, sizeof xmlbuf2)
147                 );
148         }
149
150         // FIXME ... we should implement xmpp_indicate_nonpresence so we can use it elsewhere
151
152         /* Do an unsolicited roster update that deletes the contact. */
153         cprintf("<iq from=\"%s\" to=\"%s\" id=\"unbuddy_%x\" type=\"result\">",
154                 xmlesc(xmlbuf1, CC->cs_inet_email, sizeof xmlbuf1),
155                 xmlesc(xmlbuf2, XMPP->client_jid, sizeof xmlbuf2),
156                 ++unsolicited_id
157         );
158         cprintf("<query xmlns=\"jabber:iq:roster\">");
159         cprintf("<item jid=\"%s\" subscription=\"remove\">", xmlesc(xmlbuf1, presence_jid, sizeof xmlbuf1));
160         cprintf("<group>%s</group>", xmlesc(xmlbuf1, config.c_humannode, sizeof xmlbuf1));
161         cprintf("</item>");
162         cprintf("</query>"
163                 "</iq>"
164         );
165 }
166
167
168 /*
169  * When a user logs in or out of the local Citadel system, notify all XMPP sessions about it.
170  * THIS FUNCTION HAS A BUG IN IT THAT ENUMERATES THE SESSIONS WRONG.
171  */
172 void xmpp_presence_notify(char *presence_jid, int event_type) {
173         struct CitContext *cptr;
174         static int unsolicited_id;
175         int visible_sessions = 0;
176         int nContexts, i;
177         int which_cptr_is_relevant = (-1);
178
179         if (IsEmptyStr(presence_jid)) return;
180         if (CC->kill_me) return;
181
182         cptr = CtdlGetContextArray(&nContexts);
183         if (!cptr) {
184                 return;
185         }
186
187         /* Count the visible sessions for this user */
188         for (i=0; i<nContexts; i++) {
189                 if ( (!strcasecmp(cptr[i].cs_inet_email, presence_jid))
190                    && (xmpp_is_visible(&cptr[i], CC))
191                 )  {
192                         ++visible_sessions;
193                         which_cptr_is_relevant = i;
194                 }
195         }
196
197         XMPP_syslog(LOG_DEBUG, "%d sessions for <%s> are now visible to session %d\n",
198                     visible_sessions, presence_jid, CC->cs_pid);
199
200         if ( (event_type == XMPP_EVT_LOGIN) && (visible_sessions == 1) ) {
201
202                 XMPP_syslog(LOG_DEBUG, "Telling session %d that <%s> logged in\n",
203                             CC->cs_pid, presence_jid);
204
205                 /* Do an unsolicited roster update that adds a new contact. */
206                 assert(which_cptr_is_relevant >= 0);
207                 cprintf("<iq id=\"unsolicited_%x\" type=\"result\">", ++unsolicited_id);
208                 cprintf("<query xmlns=\"jabber:iq:roster\">");
209                 xmpp_roster_item(&cptr[which_cptr_is_relevant]);
210                 cprintf("</query></iq>");
211
212                 /* Transmit presence information */
213                 xmpp_indicate_presence(presence_jid);
214         }
215
216         if (visible_sessions == 0) {
217                 XMPP_syslog(LOG_DEBUG, "Telling session %d that <%s> logged out\n",
218                             CC->cs_pid, presence_jid);
219                 xmpp_destroy_buddy(presence_jid, 0);    /* non aggressive presence update */
220         }
221
222         free(cptr);
223 }
224
225
226
227 void xmpp_fetch_mortuary_backend(long msgnum, void *userdata) {
228         HashList *mortuary = (HashList *) userdata;
229         struct CtdlMessage *msg;
230         char *ptr = NULL;
231         char *lasts = NULL;
232
233         msg = CtdlFetchMessage(msgnum, 1);
234         if (msg == NULL) {
235                 return;
236         }
237
238         /* now add anyone we find into the hashlist */
239
240         /* skip past the headers */
241         ptr = strstr(msg->cm_fields[eMesageText], "\n\n");
242         if (ptr != NULL) {
243                 ptr += 2;
244         }
245         else {
246                 ptr = strstr(msg->cm_fields[eMesageText], "\n\r\n");
247                 if (ptr != NULL) {
248                         ptr += 3;
249                 }
250         }
251
252         /* the remaining lines are addresses */
253         if (ptr != NULL) {
254                 ptr = strtok_r(ptr, "\n", &lasts);
255                 while (ptr != NULL) {
256                         char *pch = strdup(ptr);
257                         Put(mortuary, pch, strlen(pch), pch, NULL);
258                         ptr = strtok_r(NULL, "\n", &lasts);
259                 }
260         }
261
262         CM_Free(msg);
263 }
264
265
266
267 /*
268  * Fetch the "mortuary" - a list of dead buddies which we keep around forever
269  * so we can remove them from any client's roster that still has them listed
270  */
271 HashList *xmpp_fetch_mortuary(void) {
272         HashList *mortuary = NewHash(1, NULL);
273         if (!mortuary) {
274                 XMPPM_syslog(LOG_ALERT, "NewHash() failed!\n");
275                 return(NULL);
276         }
277
278         if (CtdlGetRoom(&CC->room, USERCONFIGROOM) != 0) {
279                 /* no config room exists - no further processing is required. */
280                 return(mortuary);
281         }
282         CtdlForEachMessage(MSGS_LAST, 1, NULL, XMPPMORTUARY, NULL,
283                 xmpp_fetch_mortuary_backend, (void *)mortuary );
284
285         return(mortuary);
286 }
287
288
289
290 /*
291  * Fetch the "mortuary" - a list of dead buddies which we keep around forever
292  * so we can remove them from any client's roster that still has them listed
293  */
294 void xmpp_store_mortuary(HashList *mortuary) {
295         HashPos *HashPos;
296         long len;
297         void *Value;
298         const char *Key;
299         StrBuf *themsg;
300
301         themsg = NewStrBuf();
302         StrBufPrintf(themsg,    "Content-type: " XMPPMORTUARY "\n"
303                                 "Content-transfer-encoding: 7bit\n"
304                                 "\n"
305         );
306
307         HashPos = GetNewHashPos(mortuary, 0);
308         while (GetNextHashPos(mortuary, HashPos, &len, &Key, &Value) != 0)
309         {
310                 StrBufAppendPrintf(themsg, "%s\n", (char *)Value);
311         }
312         DeleteHashPos(&HashPos);
313
314         /* FIXME temp crap 
315         StrBufAppendPrintf(themsg, "foo@bar.com\n");
316         StrBufAppendPrintf(themsg, "baz@quux.com\n");
317         StrBufAppendPrintf(themsg, "haha%c\n", 1);
318         StrBufAppendPrintf(themsg, "baaaz@quux.com\n");
319         StrBufAppendPrintf(themsg, "baaaz@quuuuuux.com\n");
320         */
321
322         /* Delete the old mortuary */
323         CtdlDeleteMessages(USERCONFIGROOM, NULL, 0, XMPPMORTUARY);
324
325         /* And save the new one to disk */
326         quickie_message("Citadel", NULL, NULL, USERCONFIGROOM, ChrPtr(themsg), 4, "XMPP Mortuary");
327         FreeStrBuf(&themsg);
328 }
329
330
331
332 /*
333  * Upon logout we make an attempt to delete the whole roster, in order to
334  * try to keep "ghost" buddies from remaining in the client-side roster.
335  *
336  * Since the client is probably not still alive, also remember the current
337  * roster for next time so we can delete dead buddies then.
338  */
339 void xmpp_massacre_roster(void)
340 {
341         struct CitContext *cptr;
342         int nContexts, i;
343         HashList *mortuary = xmpp_fetch_mortuary();
344
345         cptr = CtdlGetContextArray(&nContexts);
346         if (cptr) {
347                 for (i=0; i<nContexts; i++) {
348                         if (xmpp_is_visible(&cptr[i], CC)) {
349                                 if (mortuary) {
350                                         char *buddy = strdup(cptr[i].cs_inet_email);
351                                         Put(mortuary, buddy, strlen(buddy), buddy, NULL);
352                                 }
353                         }
354                 }
355                 free (cptr);
356         }
357
358         if (mortuary) {
359                 xmpp_store_mortuary(mortuary);
360                 DeleteHash(&mortuary);
361         }
362 }
363
364
365
366 /*
367  * Stupidly, XMPP does not specify a way to tell the client to flush its client-side roster
368  * and prepare to receive a new one.  So instead we remember every buddy we've ever told the
369  * client about, and push delete operations out at the beginning of a session.
370  * 
371  * We omit any users who happen to be online right now, but we still keep them in the mortuary,
372  * which needs to be maintained as a list of every buddy the user has ever seen.  We don't know
373  * when they're connecting from the same client and when they're connecting from a different client,
374  * so we have no guarantee of what is in the client side roster at connect time.
375  */
376 void xmpp_delete_old_buddies_who_no_longer_exist_from_the_client_roster(void)
377 {
378         long len;
379         void *Value;
380         const char *Key;
381         struct CitContext *cptr;
382         int nContexts, i;
383         int online_now = 0;
384         HashList *mortuary = xmpp_fetch_mortuary();
385         HashPos *HashPos = GetNewHashPos(mortuary, 0);
386
387         /* we need to omit anyone who is currently online */
388         cptr = CtdlGetContextArray(&nContexts);
389
390         /* go through the list of users in the mortuary... */
391         while (GetNextHashPos(mortuary, HashPos, &len, &Key, &Value) != 0)
392         {
393
394                 online_now = 0;
395                 if (cptr) for (i=0; i<nContexts; i++) {
396                         if (xmpp_is_visible(&cptr[i], CC)) {
397                                 if (!strcasecmp(cptr[i].cs_inet_email, (char *)Value)) {
398                                         online_now = 1;
399                                 }
400                         }
401                 }
402
403                 if (!online_now) {
404                         xmpp_destroy_buddy((char *)Value, 1);   /* aggressive presence update */
405                 }
406
407         }
408         DeleteHashPos(&HashPos);
409         DeleteHash(&mortuary);
410         free(cptr);
411 }
412