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