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