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