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