a061fc8465c720b0f955dcc10021d3011d69aa2a
[citadel.git] / citadel / modules / instmsg / serv_instmsg.c
1 /*
2  * $Id$
3  *
4  * This module handles instant messaging between users.
5  * 
6  * Copyright (c) 1987-2010 by the citadel.org team
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  *
22  */
23 #include "sysdep.h"
24 #include <stdlib.h>
25 #include <unistd.h>
26 #include <stdio.h>
27 #include <fcntl.h>
28 #include <signal.h>
29 #include <pwd.h>
30 #include <errno.h>
31 #include <sys/types.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 <libcitadel.h>
48 #include "citadel.h"
49 #include "server.h"
50 #include "serv_instmsg.h"
51 #include "citserver.h"
52 #include "support.h"
53 #include "config.h"
54 #include "msgbase.h"
55 #include "user_ops.h"
56
57 #ifndef HAVE_SNPRINTF
58 #include "snprintf.h"
59 #endif
60
61 #include "ctdl_module.h"
62
63 struct imlog {
64         struct imlog *next;
65         long usernums[2];
66         char usernames[2][128];
67         time_t lastmsg;
68         int last_serial;
69         StrBuf *conversation;
70 };
71
72 struct imlog *imlist = NULL;
73
74 /*
75  * This function handles the logging of instant messages to disk.
76  */
77 void log_instant_message(struct CitContext *me, struct CitContext *them, char *msgtext, int serial_number)
78 {
79         long usernums[2];
80         long t;
81         struct imlog *iptr = NULL;
82         struct imlog *this_im = NULL;
83         
84         memset(usernums, 0, sizeof usernums);
85         usernums[0] = me->user.usernum;
86         usernums[1] = them->user.usernum;
87
88         /* Always put the lower user number first, so we can use the array as a hash value which
89          * represents a pair of users.  For a broadcast message one of the users will be 0.
90          */
91         if (usernums[0] > usernums[1]) {
92                 t = usernums[0];
93                 usernums[0] = usernums[1];
94                 usernums[1] = t;
95         }
96
97         begin_critical_section(S_IM_LOGS);
98
99         /* Look for an existing conversation in the hash table.
100          * If not found, create a new one.
101          */
102
103         this_im = NULL;
104         for (iptr = imlist; iptr != NULL; iptr = iptr->next) {
105                 if ((iptr->usernums[0] == usernums[0]) && (iptr->usernums[1] == usernums[1])) {
106                         /* Existing conversation */
107                         this_im = iptr;
108                 }
109         }
110         if (this_im == NULL) {
111                 /* New conversation */
112                 this_im = malloc(sizeof(struct imlog));
113                 memset(this_im, 0, sizeof (struct imlog));
114                 this_im->usernums[0] = usernums[0];
115                 this_im->usernums[1] = usernums[1];
116                 /* usernames[] and usernums[] might not be in the same order.  This is not an error. */
117                 if (me) {
118                         safestrncpy(this_im->usernames[0], me->user.fullname, sizeof this_im->usernames[0]);
119                 }
120                 if (them) {
121                         safestrncpy(this_im->usernames[1], them->user.fullname, sizeof this_im->usernames[1]);
122                 }
123                 this_im->conversation = NewStrBuf();
124                 this_im->next = imlist;
125                 imlist = this_im;
126                 StrBufAppendBufPlain(this_im->conversation, HKEY(
127                         "Content-type: text/html\r\n"
128                         "Content-transfer-encoding: 7bit\r\n"
129                         "\r\n"
130                         "<html><body>\r\n"
131                         ), 0);
132         }
133
134
135         /* Since it's possible for this function to get called more than once if a user is logged
136          * in on multiple sessions, we use the message's serial number to keep track of whether
137          * we've already logged it.
138          */
139         if (this_im->last_serial != serial_number)
140         {
141                 this_im->lastmsg = time(NULL);          /* Touch the timestamp so we know when to flush */
142                 this_im->last_serial = serial_number;
143                 StrBufAppendBufPlain(this_im->conversation, HKEY("<p><b>"), 0);
144                 StrBufAppendBufPlain(this_im->conversation, me->user.fullname, -1, 0);
145                 StrBufAppendBufPlain(this_im->conversation, HKEY(":</b> "), 0);
146                 StrEscAppend(this_im->conversation, NULL, msgtext, 0, 0);
147                 StrBufAppendBufPlain(this_im->conversation, HKEY("</p>\r\n"), 0);
148         }
149         end_critical_section(S_IM_LOGS);
150 }
151
152
153 /*
154  * Delete any remaining instant messages
155  */
156 void delete_instant_messages(void) {
157         struct ExpressMessage *ptr;
158
159         begin_critical_section(S_SESSION_TABLE);
160         while (CC->FirstExpressMessage != NULL) {
161                 ptr = CC->FirstExpressMessage->next;
162                 if (CC->FirstExpressMessage->text != NULL)
163                         free(CC->FirstExpressMessage->text);
164                 free(CC->FirstExpressMessage);
165                 CC->FirstExpressMessage = ptr;
166         }
167         end_critical_section(S_SESSION_TABLE);
168 }
169
170
171
172 /*
173  * Retrieve instant messages
174  */
175 void cmd_gexp(char *argbuf) {
176         struct ExpressMessage *ptr;
177
178         if (CC->FirstExpressMessage == NULL) {
179                 cprintf("%d No instant messages waiting.\n", ERROR + MESSAGE_NOT_FOUND);
180                 return;
181         }
182
183         begin_critical_section(S_SESSION_TABLE);
184         ptr = CC->FirstExpressMessage;
185         CC->FirstExpressMessage = CC->FirstExpressMessage->next;
186         end_critical_section(S_SESSION_TABLE);
187
188         cprintf("%d %d|%ld|%d|%s|%s|%s\n",
189                 LISTING_FOLLOWS,
190                 ((ptr->next != NULL) ? 1 : 0),          /* more msgs? */
191                 (long)ptr->timestamp,                   /* time sent */
192                 ptr->flags,                             /* flags */
193                 ptr->sender,                            /* sender of msg */
194                 config.c_nodename,                      /* static for now (and possibly deprecated) */
195                 ptr->sender_email                       /* email or jid of sender */
196         );
197
198         if (ptr->text != NULL) {
199                 memfmout(ptr->text, "\n");
200                 free(ptr->text);
201         }
202
203         cprintf("000\n");
204         free(ptr);
205 }
206
207 /*
208  * Asynchronously deliver instant messages
209  */
210 void cmd_gexp_async(void) {
211
212         /* Only do this if the session can handle asynchronous protocol */
213         if (CC->is_async == 0) return;
214
215         /* And don't do it if there's nothing to send. */
216         if (CC->FirstExpressMessage == NULL) return;
217
218         cprintf("%d instant msg\n", ASYNC_MSG + ASYNC_GEXP);
219 }
220
221 /*
222  * Back end support function for send_instant_message() and company
223  */
224 void add_xmsg_to_context(struct CitContext *ccptr, struct ExpressMessage *newmsg) 
225 {
226         struct ExpressMessage *findend;
227
228         if (ccptr->FirstExpressMessage == NULL) {
229                 ccptr->FirstExpressMessage = newmsg;
230         }
231         else {
232                 findend = ccptr->FirstExpressMessage;
233                 while (findend->next != NULL) {
234                         findend = findend->next;
235                 }
236                 findend->next = newmsg;
237         }
238
239         /* If the target context is a session which can handle asynchronous
240          * messages, go ahead and set the flag for that.
241          */
242         set_async_waiting(ccptr);
243 }
244
245
246
247
248 /* 
249  * This is the back end to the instant message sending function.  
250  * Returns the number of users to which the message was sent.
251  * Sending a zero-length message tests for recipients without sending messages.
252  */
253 int send_instant_message(char *lun, char *lem, char *x_user, char *x_msg)
254 {
255         int message_sent = 0;           /* number of successful sends */
256         struct CitContext *ccptr;
257         struct ExpressMessage *newmsg = NULL;
258         char *un;
259         int do_send = 0;                /* 1 = send message; 0 = only check for valid recipient */
260         static int serial_number = 0;   /* this keeps messages from getting logged twice */
261
262         if (strlen(x_msg) > 0) {
263                 do_send = 1;
264         }
265
266         /* find the target user's context and append the message */
267         begin_critical_section(S_SESSION_TABLE);
268         ++serial_number;
269         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
270
271                 if (ccptr->fake_username[0]) {
272                         un = ccptr->fake_username;
273                 }
274                 else {
275                         un = ccptr->user.fullname;
276                 }
277
278                 if ( ((!strcasecmp(un, x_user))
279                     || (!strcasecmp(x_user, "broadcast")))
280                     && (ccptr->can_receive_im)
281                     && ((ccptr->disable_exp == 0)
282                     || (CC->user.axlevel >= AxAideU)) ) {
283                         if (do_send) {
284                                 newmsg = (struct ExpressMessage *) malloc(sizeof (struct ExpressMessage));
285                                 memset(newmsg, 0, sizeof (struct ExpressMessage));
286                                 time(&(newmsg->timestamp));
287                                 safestrncpy(newmsg->sender, lun, sizeof newmsg->sender);
288                                 safestrncpy(newmsg->sender_email, lem, sizeof newmsg->sender_email);
289                                 if (!strcasecmp(x_user, "broadcast")) {
290                                         newmsg->flags |= EM_BROADCAST;
291                                 }
292                                 newmsg->text = strdup(x_msg);
293
294                                 add_xmsg_to_context(ccptr, newmsg);
295
296                                 /* and log it ... */
297                                 if (ccptr != CC) {
298                                         log_instant_message(CC, ccptr, newmsg->text, serial_number);
299                                 }
300                         }
301                         ++message_sent;
302                 }
303         }
304         end_critical_section(S_SESSION_TABLE);
305         return (message_sent);
306 }
307
308 /*
309  * send instant messages
310  */
311 void cmd_sexp(char *argbuf)
312 {
313         int message_sent = 0;
314         char x_user[USERNAME_SIZE];
315         char x_msg[1024];
316         char *lun;
317         char *lem;
318         char *x_big_msgbuf = NULL;
319
320         if ((!(CC->logged_in)) && (!(CC->internal_pgm))) {
321                 cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN);
322                 return;
323         }
324         if (CC->fake_username[0])
325                 lun = CC->fake_username;
326         else
327                 lun = CC->user.fullname;
328
329         lem = CC->cs_inet_email;
330
331         extract_token(x_user, argbuf, 0, '|', sizeof x_user);
332         extract_token(x_msg, argbuf, 1, '|', sizeof x_msg);
333
334         if (!x_user[0]) {
335                 cprintf("%d You were not previously paged.\n", ERROR + NO_SUCH_USER);
336                 return;
337         }
338         if ((!strcasecmp(x_user, "broadcast")) && (CC->user.axlevel < AxAideU)) {
339                 cprintf("%d Higher access required to send a broadcast.\n",
340                         ERROR + HIGHER_ACCESS_REQUIRED);
341                 return;
342         }
343         /* This loop handles text-transfer pages */
344         if (!strcmp(x_msg, "-")) {
345                 message_sent = PerformXmsgHooks(lun, lem, x_user, "");
346                 if (message_sent == 0) {
347                         if (CtdlGetUser(NULL, x_user))
348                                 cprintf("%d '%s' does not exist.\n",
349                                                 ERROR + NO_SUCH_USER, x_user);
350                         else
351                                 cprintf("%d '%s' is not logged in "
352                                                 "or is not accepting pages.\n",
353                                                 ERROR + RESOURCE_NOT_OPEN, x_user);
354                         return;
355                 }
356                 unbuffer_output();
357                 cprintf("%d Transmit message (will deliver to %d users)\n",
358                         SEND_LISTING, message_sent);
359                 x_big_msgbuf = malloc(SIZ);
360                 memset(x_big_msgbuf, 0, SIZ);
361                 while (client_getln(x_msg, sizeof x_msg) >= 0 && strcmp(x_msg, "000")) {
362                         x_big_msgbuf = realloc(x_big_msgbuf,
363                                strlen(x_big_msgbuf) + strlen(x_msg) + 4);
364                         if (!IsEmptyStr(x_big_msgbuf))
365                            if (x_big_msgbuf[strlen(x_big_msgbuf)] != '\n')
366                                 strcat(x_big_msgbuf, "\n");
367                         strcat(x_big_msgbuf, x_msg);
368                 }
369                 PerformXmsgHooks(lun, lem, x_user, x_big_msgbuf);
370                 free(x_big_msgbuf);
371
372                 /* This loop handles inline pages */
373         } else {
374                 message_sent = PerformXmsgHooks(lun, lem, x_user, x_msg);
375
376                 if (message_sent > 0) {
377                         if (!IsEmptyStr(x_msg))
378                                 cprintf("%d Message sent", CIT_OK);
379                         else
380                                 cprintf("%d Ok to send message", CIT_OK);
381                         if (message_sent > 1)
382                                 cprintf(" to %d users", message_sent);
383                         cprintf(".\n");
384                 } else {
385                         if (CtdlGetUser(NULL, x_user))
386                                 cprintf("%d '%s' does not exist.\n",
387                                                 ERROR + NO_SUCH_USER, x_user);
388                         else
389                                 cprintf("%d '%s' is not logged in "
390                                                 "or is not accepting pages.\n",
391                                                 ERROR + RESOURCE_NOT_OPEN, x_user);
392                 }
393
394
395         }
396 }
397
398
399
400 /*
401  * Enter or exit paging-disabled mode
402  */
403 void cmd_dexp(char *argbuf)
404 {
405         int new_state;
406
407         if (CtdlAccessCheck(ac_logged_in)) return;
408
409         new_state = extract_int(argbuf, 0);
410         if ((new_state == 0) || (new_state == 1)) {
411                 CC->disable_exp = new_state;
412         }
413
414         cprintf("%d %d\n", CIT_OK, CC->disable_exp);
415 }
416
417
418 /*
419  * Request client termination
420  */
421 void cmd_reqt(char *argbuf) {
422         struct CitContext *ccptr;
423         int sessions = 0;
424         int which_session;
425         struct ExpressMessage *newmsg;
426
427         if (CtdlAccessCheck(ac_aide)) return;
428         which_session = extract_int(argbuf, 0);
429
430         begin_critical_section(S_SESSION_TABLE);
431         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
432                 if ((ccptr->cs_pid == which_session) || (which_session == 0)) {
433
434                         newmsg = (struct ExpressMessage *)
435                                 malloc(sizeof (struct ExpressMessage));
436                         memset(newmsg, 0,
437                                 sizeof (struct ExpressMessage));
438                         time(&(newmsg->timestamp));
439                         safestrncpy(newmsg->sender, CC->user.fullname,
440                                     sizeof newmsg->sender);
441                         newmsg->flags |= EM_GO_AWAY;
442                         newmsg->text = strdup("Automatic logoff requested.");
443
444                         add_xmsg_to_context(ccptr, newmsg);
445                         ++sessions;
446
447                 }
448         }
449         end_critical_section(S_SESSION_TABLE);
450         cprintf("%d Sent termination request to %d sessions.\n", CIT_OK, sessions);
451 }
452
453
454 /*
455  * This is the back end for flush_conversations_to_disk()
456  * At this point we've isolated a single conversation (struct imlog)
457  * and are ready to write it to disk.
458  */
459 void flush_individual_conversation(struct imlog *im) {
460         struct CtdlMessage *msg;
461         long msgnum = 0;
462         char roomname[ROOMNAMELEN];
463
464         StrBufAppendBufPlain(im->conversation, HKEY(
465                 "</body>\r\n"
466                 "</html>\r\n"
467                 ), 0
468         );
469
470         msg = malloc(sizeof(struct CtdlMessage));
471         memset(msg, 0, sizeof(struct CtdlMessage));
472         msg->cm_magic = CTDLMESSAGE_MAGIC;
473         msg->cm_anon_type = MES_NORMAL;
474         msg->cm_format_type = FMT_RFC822;
475         if (!IsEmptyStr(im->usernames[0])) {
476                 msg->cm_fields['A'] = strdup(im->usernames[0]);
477         } else {
478                 msg->cm_fields['A'] = strdup("Citadel");
479         }
480         if (!IsEmptyStr(im->usernames[1])) {
481                 msg->cm_fields['R'] = strdup(im->usernames[1]);
482         }
483         msg->cm_fields['O'] = strdup(PAGELOGROOM);
484         msg->cm_fields['N'] = strdup(NODENAME);
485         msg->cm_fields['M'] = SmashStrBuf(&im->conversation);   /* we own this memory now */
486
487         /* Start with usernums[1] because it's guaranteed to be higher than usernums[0],
488          * so if there's only one party, usernums[0] will be zero but usernums[1] won't.
489          * Create the room if necessary.  Note that we create as a type 5 room rather
490          * than 4, which indicates that it's a personal room but we've already supplied
491          * the namespace prefix.
492          *
493          * In the unlikely event that usernums[1] is zero, a room with an invalid namespace
494          * prefix will be created.  That's ok because the auto-purger will clean it up later.
495          */
496         snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[1], PAGELOGROOM);
497         CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
498         msgnum = CtdlSubmitMsg(msg, NULL, roomname, 0);
499         CtdlFreeMessage(msg);
500
501         /* If there is a valid user number in usernums[0], save a copy for them too. */
502         if (im->usernums[0] > 0) {
503                 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[0], PAGELOGROOM);
504                 CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
505                 CtdlSaveMsgPointerInRoom(roomname, msgnum, 0, NULL);
506         }
507
508         /* Finally, if we're logging instant messages globally, do that now. */
509         if (!IsEmptyStr(config.c_logpages)) {
510                 CtdlCreateRoom(config.c_logpages, 3, "", 0, 1, 1, VIEW_BBS);
511                 CtdlSaveMsgPointerInRoom(config.c_logpages, msgnum, 0, NULL);
512         }
513
514 }
515
516 /*
517  * Locate instant message conversations which have gone idle
518  * (or, if the server is shutting down, locate *all* conversations)
519  * and flush them to disk (in the participants' log rooms, etc.)
520  */
521 void flush_conversations_to_disk(time_t if_older_than) {
522
523         struct imlog *flush_these = NULL;
524         struct imlog *dont_flush_these = NULL;
525         struct imlog *imptr = NULL;
526         struct CitContext *nptr;
527         int nContexts, i;
528
529         nptr = CtdlGetContextArray(&nContexts) ;        /* Make a copy of the current wholist */
530
531         begin_critical_section(S_IM_LOGS);
532         while (imlist)
533         {
534                 imptr = imlist;
535                 imlist = imlist->next;
536
537                 /* For a two party conversation, if one party has logged out, force flush. */
538                 if (nptr) {
539                         int user0_is_still_online = 0;
540                         int user1_is_still_online = 0;
541                         for (i=0; i<nContexts; i++)  {
542                                 if (nptr[i].user.usernum == imptr->usernums[0]) ++user0_is_still_online;
543                                 if (nptr[i].user.usernum == imptr->usernums[1]) ++user1_is_still_online;
544                         }
545                         if (imptr->usernums[0] != imptr->usernums[1]) {         /* two party conversation */
546                                 if ((!user0_is_still_online) || (!user1_is_still_online)) {
547                                         imptr->lastmsg = 0L;    /* force flush */
548                                 }
549                         }
550                         else {          /* one party conversation (yes, people do IM themselves) */
551                                 if (!user0_is_still_online) {
552                                         imptr->lastmsg = 0L;    /* force flush */
553                                 }
554                         }
555                 }
556
557                 /* Now test this conversation to see if it qualifies for flushing. */
558                 if ((time(NULL) - imptr->lastmsg) > if_older_than)
559                 {
560                         /* This conversation qualifies.  Move it to the list of ones to flush. */
561                         imptr->next = flush_these;
562                         flush_these = imptr;
563                 }
564                 else  {
565                         /* Move it to the list of ones not to flush. */
566                         imptr->next = dont_flush_these;
567                         dont_flush_these = imptr;
568                 }
569         }
570         imlist = dont_flush_these;
571         end_critical_section(S_IM_LOGS);
572         free(nptr);
573
574         /* We are now outside of the critical section, and we are the only thread holding a
575          * pointer to a linked list of conversations to be flushed to disk.
576          */
577         while (flush_these) {
578
579                 flush_individual_conversation(flush_these);     /* This will free the string buffer */
580                 imptr = flush_these;
581                 flush_these = flush_these->next;
582                 free(imptr);
583         }
584 }
585
586
587
588 void instmsg_timer(void) {
589         flush_conversations_to_disk(300);       /* Anything that hasn't peeped in more than 5 minutes */
590 }
591
592 void instmsg_shutdown(void) {
593         flush_conversations_to_disk(0);         /* Get it ALL onto disk NOW. */
594 }
595
596 CTDL_MODULE_INIT(instmsg)
597 {
598         if (!threading)
599         {
600                 CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
601                 CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
602                 CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
603                 CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
604                 CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC);
605                 CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP);
606                 CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
607                 CtdlRegisterSessionHook(instmsg_timer, EVT_TIMER);
608                 CtdlRegisterSessionHook(instmsg_shutdown, EVT_SHUTDOWN);
609         }
610         
611         /* return our Subversion id for the Log */
612         return "$Id$";
613 }