3af65c09fb2370dbfe38be2c3094f405e82172a5
[citadel.git] / citadel / modules / chat / serv_chat.c
1 /*
2  * $Id$
3  * 
4  * This module handles all "real time" communication between users.  The
5  * modes of communication currently supported are Chat and Paging.
6  *
7  */
8 #include "sysdep.h"
9 #include <stdlib.h>
10 #include <unistd.h>
11 #include <stdio.h>
12 #include <fcntl.h>
13 #include <signal.h>
14 #include <pwd.h>
15 #include <errno.h>
16 #include <sys/types.h>
17
18 #if TIME_WITH_SYS_TIME
19 # include <sys/time.h>
20 # include <time.h>
21 #else
22 # if HAVE_SYS_TIME_H
23 #  include <sys/time.h>
24 # else
25 #  include <time.h>
26 # endif
27 #endif
28
29 #include <sys/wait.h>
30 #include <string.h>
31 #include <limits.h>
32 #include <libcitadel.h>
33 #include "citadel.h"
34 #include "server.h"
35 #include "serv_chat.h"
36 #include "citserver.h"
37 #include "support.h"
38 #include "config.h"
39 #include "msgbase.h"
40 #include "user_ops.h"
41 #include "room_ops.h"
42
43 #ifndef HAVE_SNPRINTF
44 #include "snprintf.h"
45 #endif
46
47 #include "ctdl_module.h"
48
49 struct ChatLine *ChatQueue = NULL;
50 int ChatLastMsg = 0;
51
52 struct imlog {
53         struct imlog *next;
54         long usernums[2];
55         char usernames[2][128];
56         time_t lastmsg;
57         int last_serial;
58         StrBuf *conversation;
59 };
60
61 struct imlog *imlist = NULL;
62
63 /*
64  * This function handles the logging of instant messages to disk.
65  */
66 void log_instant_message(struct CitContext *me, struct CitContext *them, char *msgtext, int serial_number)
67 {
68         long usernums[2];
69         long t;
70         struct imlog *iptr = NULL;
71         struct imlog *this_im = NULL;
72         
73         memset(usernums, 0, sizeof usernums);
74         usernums[0] = me->user.usernum;
75         usernums[1] = them->user.usernum;
76
77         /* Always put the lower user number first, so we can use the array as a hash value which
78          * represents a pair of users.  For a broadcast message one of the users will be 0.
79          */
80         if (usernums[0] > usernums[1]) {
81                 t = usernums[0];
82                 usernums[0] = usernums[1];
83                 usernums[1] = t;
84         }
85
86         begin_critical_section(S_IM_LOGS);
87
88         /* Look for an existing conversation in the hash table.
89          * If not found, create a new one.
90          */
91
92         this_im = NULL;
93         for (iptr = imlist; iptr != NULL; iptr = iptr->next) {
94                 if ((iptr->usernums[0] == usernums[0]) && (iptr->usernums[1] == usernums[1])) {
95                         /* Existing conversation */
96                         this_im = iptr;
97                 }
98         }
99         if (this_im == NULL) {
100                 /* New conversation */
101                 this_im = malloc(sizeof(struct imlog));
102                 memset(this_im, 0, sizeof (struct imlog));
103                 this_im->usernums[0] = usernums[0];
104                 this_im->usernums[1] = usernums[1];
105                 /* usernames[] and usernums[] might not be in the same order.  This is not an error. */
106                 if (me) {
107                         safestrncpy(this_im->usernames[0], me->user.fullname, sizeof this_im->usernames[0]);
108                 }
109                 if (them) {
110                         safestrncpy(this_im->usernames[1], them->user.fullname, sizeof this_im->usernames[1]);
111                 }
112                 this_im->conversation = NewStrBuf();
113                 this_im->next = imlist;
114                 imlist = this_im;
115                 StrBufAppendBufPlain(this_im->conversation, HKEY(
116                         "Content-type: text/html\r\n"
117                         "Content-transfer-encoding: 7bit\r\n"
118                         "\r\n"
119                         "<html><body>\r\n"
120                         ), 0);
121         }
122
123
124         /* Since it's possible for this function to get called more than once if a user is logged
125          * in on multiple sessions, we use the message's serial number to keep track of whether
126          * we've already logged it.
127          */
128         if (this_im->last_serial != serial_number)
129         {
130                 this_im->lastmsg = time(NULL);          /* Touch the timestamp so we know when to flush */
131                 this_im->last_serial = serial_number;
132                 StrBufAppendBufPlain(this_im->conversation, HKEY("<p><b>"), 0);
133                 StrBufAppendBufPlain(this_im->conversation, me->user.fullname, -1, 0);
134                 StrBufAppendBufPlain(this_im->conversation, HKEY(":</b> "), 0);
135                 StrEscAppend(this_im->conversation, NULL, msgtext, 0, 0);
136                 StrBufAppendBufPlain(this_im->conversation, HKEY("</p>\r\n"), 0);
137         }
138         end_critical_section(S_IM_LOGS);
139 }
140
141 /*
142  * This message can be set to anything you want, but it is
143  * checked for consistency so don't move it away from here.
144  */
145 #define KICKEDMSG "You have been kicked out of this room."
146
147 void allwrite(char *cmdbuf, int flag, char *username)
148 {
149         FILE *fp;
150         char bcast[SIZ];
151         char *un;
152         struct ChatLine *clptr, *clnew;
153         time_t now;
154
155         if (CC->fake_username[0])
156                 un = CC->fake_username;
157         else
158                 un = CC->user.fullname;
159         if (flag == 1) {
160                 snprintf(bcast, sizeof bcast, ":|<%s %s>", un, cmdbuf);
161         } else if (flag == 0) {
162                 snprintf(bcast, sizeof bcast, "%s|%s", un, cmdbuf);
163         } else if (flag == 2) {
164                 snprintf(bcast, sizeof bcast, ":|<%s whispers %s>", un, cmdbuf);
165         } else if (flag == 3) {
166                 snprintf(bcast, sizeof bcast, ":|%s", KICKEDMSG);
167         }
168         if ((strcasecmp(cmdbuf, "NOOP")) && (flag != 2)) {
169                 fp = fopen(CHATLOG, "a");
170                 if (fp != NULL)
171                         fprintf(fp, "%s\n", bcast);
172                 fclose(fp);
173         }
174         clnew = (struct ChatLine *) malloc(sizeof(struct ChatLine));
175         memset(clnew, 0, sizeof(struct ChatLine));
176         if (clnew == NULL) {
177                 fprintf(stderr, "citserver: cannot alloc chat line: %s\n",
178                         strerror(errno));
179                 return;
180         }
181         time(&now);
182         clnew->next = NULL;
183         clnew->chat_time = now;
184         safestrncpy(clnew->chat_room, CC->room.QRname,
185                         sizeof clnew->chat_room);
186         clnew->chat_room[sizeof clnew->chat_room - 1] = 0;
187         if (username) {
188                 safestrncpy(clnew->chat_username, username,
189                         sizeof clnew->chat_username);
190                 clnew->chat_username[sizeof clnew->chat_username - 1] = 0;
191         } else
192                 clnew->chat_username[0] = '\0';
193         safestrncpy(clnew->chat_text, bcast, sizeof clnew->chat_text);
194
195         /* Here's the critical section.
196          * First, add the new message to the queue...
197          */
198         begin_critical_section(S_CHATQUEUE);
199         ++ChatLastMsg;
200         clnew->chat_seq = ChatLastMsg;
201         if (ChatQueue == NULL) {
202                 ChatQueue = clnew;
203         } else {
204                 for (clptr = ChatQueue; clptr->next != NULL; clptr = clptr->next);;
205                 clptr->next = clnew;
206         }
207
208         /* Then, before releasing the lock, free the expired messages */
209         while ((ChatQueue != NULL) && (now - ChatQueue->chat_time >= 120L)) {
210                 clptr = ChatQueue;
211                 ChatQueue = ChatQueue->next;
212                 free(clptr);
213         }
214         end_critical_section(S_CHATQUEUE);
215 }
216
217
218 t_context *find_context(char **unstr)
219 {
220         t_context *t_cc, *found_cc = NULL;
221         char *name, *tptr;
222
223         if ((!*unstr) || (!unstr))
224                 return (NULL);
225
226         begin_critical_section(S_SESSION_TABLE);
227         for (t_cc = ContextList; ((t_cc) && (!found_cc)); t_cc = t_cc->next) {
228                 if (t_cc->fake_username[0])
229                         name = t_cc->fake_username;
230                 else
231                         name = t_cc->curr_user;
232                 tptr = *unstr;
233                 if ((!strncasecmp(name, tptr, strlen(name))) && (tptr[strlen(name)] == ' ')) {
234                         found_cc = t_cc;
235                         *unstr = &(tptr[strlen(name) + 1]);
236                 }
237         }
238         end_critical_section(S_SESSION_TABLE);
239
240         return (found_cc);
241 }
242
243 /*
244  * List users in chat.
245  * allflag ==   0 = list users in chat
246  *              1 = list users in chat, followed by users not in chat
247  *              2 = display count only
248  */
249
250 void do_chat_listing(int allflag)
251 {
252         struct CitContext *ccptr;
253         int count = 0;
254         int count_elsewhere = 0;
255         char roomname[ROOMNAMELEN];
256
257         if ((allflag == 0) || (allflag == 1))
258                 cprintf(":|\n:| Users currently in chat:\n");
259         begin_critical_section(S_SESSION_TABLE);
260         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
261                 if (ccptr->cs_flags & CS_CHAT) {
262                         if (!strcasecmp(ccptr->room.QRname,
263                            CC->room.QRname)) {
264                                 ++count;
265                         }
266                         else {
267                                 ++count_elsewhere;
268                         }
269                 }
270
271                 GenerateRoomDisplay(roomname, ccptr, CC);
272                 if ((CC->user.axlevel < 6) && (!IsEmptyStr(ccptr->fake_roomname))) {
273                         strcpy(roomname, ccptr->fake_roomname);
274                 }
275
276                 if ((ccptr->cs_flags & CS_CHAT) && ((ccptr->cs_flags & CS_STEALTH) == 0)) {
277                         if ((allflag == 0) || (allflag == 1)) {
278                                 cprintf(":| %-25s <%s>:\n",
279                                         (ccptr->fake_username[0]) ? ccptr->fake_username : ccptr->curr_user,
280                                         roomname);
281                         }
282                 }
283         }
284
285         if (allflag == 1) {
286                 cprintf(":|\n:| Users not in chat:\n");
287                 for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
288
289                         GenerateRoomDisplay(roomname, ccptr, CC);
290                         if ((CC->user.axlevel < 6)
291                         && (!IsEmptyStr(ccptr->fake_roomname))) {
292                                 strcpy(roomname, ccptr->fake_roomname);
293                         }
294
295                         if (((ccptr->cs_flags & CS_CHAT) == 0)
296                             && ((ccptr->cs_flags & CS_STEALTH) == 0)) {
297                                 cprintf(":| %-25s <%s>:\n",
298                                         (ccptr->fake_username[0]) ? ccptr->fake_username : ccptr->curr_user,
299                                         roomname);
300                         }
301                 }
302         }
303         end_critical_section(S_SESSION_TABLE);
304
305         if (allflag == 2) {
306                 if (count > 1) {
307                         cprintf(":|There are %d users here.\n", count);
308                 }
309                 else {
310                         cprintf(":|Note: you are the only one here.\n");
311                 }
312                 if (count_elsewhere > 0) {
313                         cprintf(":|There are %d users chatting in other rooms.\n", count_elsewhere);
314                 }
315         }
316
317         cprintf(":|\n");
318 }
319
320
321 void cmd_chat(char *argbuf)
322 {
323         char cmdbuf[SIZ];
324         char *un;
325         char *strptr1;
326         int MyLastMsg, ThisLastMsg;
327         struct ChatLine *clptr;
328         struct CitContext *t_context;
329         int retval;
330
331         if (!(CC->logged_in)) {
332                 cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN);
333                 return;
334         }
335
336         CC->cs_flags = CC->cs_flags | CS_CHAT;
337         cprintf("%d Entering chat mode (type '/help' for available commands)\n",
338                 START_CHAT_MODE);
339         unbuffer_output();
340
341         MyLastMsg = ChatLastMsg;
342
343         if ((CC->cs_flags & CS_STEALTH) == 0) {
344                 allwrite("<entering chat>", 0, NULL);
345         }
346         strcpy(cmdbuf, "");
347
348         do_chat_listing(2);
349
350         while (1) {
351                 int ok_cmd;
352                 int linelen;
353
354                 ok_cmd = 0;
355                 linelen = strlen(cmdbuf);
356                 if (linelen > 100) --linelen;   /* truncate too-long lines */
357                 cmdbuf[linelen + 1] = 0;
358
359                 retval = client_read_to(&cmdbuf[linelen], 1, 2);
360
361                 if (retval < 0 || CC->kill_me) {        /* socket broken? */
362                         if ((CC->cs_flags & CS_STEALTH) == 0) {
363                                 allwrite("<disconnected>", 0, NULL);
364                         }
365                         return;
366                 }
367
368                 /* if we have a complete line, do send processing */
369                 if (!IsEmptyStr(cmdbuf))
370                         if (cmdbuf[strlen(cmdbuf) - 1] == 10) {
371                                 cmdbuf[strlen(cmdbuf) - 1] = 0;
372                                 time(&CC->lastcmd);
373                                 time(&CC->lastidle);
374
375                                 if ((!strcasecmp(cmdbuf, "exit"))
376                                     || (!strcasecmp(cmdbuf, "/exit"))
377                                     || (!strcasecmp(cmdbuf, "quit"))
378                                     || (!strcasecmp(cmdbuf, "logout"))
379                                     || (!strcasecmp(cmdbuf, "logoff"))
380                                     || (!strcasecmp(cmdbuf, "/q"))
381                                     || (!strcasecmp(cmdbuf, ".q"))
382                                     || (!strcasecmp(cmdbuf, "/quit"))
383                                     )
384                                         strcpy(cmdbuf, "000");
385
386                                 if (!strcmp(cmdbuf, "000")) {
387                                         if ((CC->cs_flags & CS_STEALTH) == 0) {
388                                                 allwrite("<exiting chat>", 0, NULL);
389                                         }
390                                         sleep(1);
391                                         cprintf("000\n");
392                                         CC->cs_flags = CC->cs_flags - CS_CHAT;
393                                         return;
394                                 }
395                                 if ((!strcasecmp(cmdbuf, "/help"))
396                                     || (!strcasecmp(cmdbuf, "help"))
397                                     || (!strcasecmp(cmdbuf, "/?"))
398                                     || (!strcasecmp(cmdbuf, "?"))) {
399                                         cprintf(":|\n");
400                                         cprintf(":|Available commands: \n");
401                                         cprintf(":|/help   (prints this message) \n");
402                                         cprintf(":|/who    (list users currently in chat) \n");
403                                         cprintf(":|/whobbs (list users in chat -and- elsewhere) \n");
404                                         cprintf(":|/me     ('action' line, ala irc) \n");
405                                         cprintf(":|/msg    (send private message, ala irc) \n");
406                                         if (is_room_aide()) {
407                                                 cprintf(":|/kick   (kick another user out of this room) \n");
408                                         }
409                                         cprintf(":|/quit   (exit from this chat) \n");
410                                         cprintf(":|\n");
411                                         ok_cmd = 1;
412                                 }
413                                 if (!strcasecmp(cmdbuf, "/who")) {
414                                         do_chat_listing(0);
415                                         ok_cmd = 1;
416                                 }
417                                 if (!strcasecmp(cmdbuf, "/whobbs")) {
418                                         do_chat_listing(1);
419                                         ok_cmd = 1;
420                                 }
421                                 if (!strncasecmp(cmdbuf, "/me ", 4)) {
422                                         allwrite(&cmdbuf[4], 1, NULL);
423                                         ok_cmd = 1;
424                                 }
425                                 if (!strncasecmp(cmdbuf, "/msg ", 5)) {
426                                         ok_cmd = 1;
427                                         strptr1 = &cmdbuf[5];
428                                         if ((t_context = find_context(&strptr1))) {
429                                                 allwrite(strptr1, 2, CC->curr_user);
430                                                 if (strcasecmp(CC->curr_user, t_context->curr_user))
431                                                         allwrite(strptr1, 2, t_context->curr_user);
432                                         } else
433                                                 cprintf(":|User not found.\n");
434                                         cprintf("\n");
435                                 }
436                                 /* The /kick function is implemented by sending a specific
437                                  * message to the kicked-out user's context.  When that message
438                                  * is processed by the read loop, that context will exit.
439                                  */
440                                 if ( (!strncasecmp(cmdbuf, "/kick ", 6)) && (is_room_aide()) ) {
441                                         ok_cmd = 1;
442                                         strptr1 = &cmdbuf[6];
443                                         strcat(strptr1, " ");
444                                         if ((t_context = find_context(&strptr1))) {
445                                                 if (strcasecmp(CC->curr_user, t_context->curr_user))
446                                                         allwrite(strptr1, 3, t_context->curr_user);
447                                         } else
448                                                 cprintf(":|User not found.\n");
449                                         cprintf("\n");
450                                 }
451                                 if ((cmdbuf[0] != '/') && (strlen(cmdbuf) > 0)) {
452                                         ok_cmd = 1;
453                                         allwrite(cmdbuf, 0, NULL);
454                                 }
455                                 if ((!ok_cmd) && (cmdbuf[0]) && (cmdbuf[0] != '\n'))
456                                         cprintf(":|Command %s is not understood.\n", cmdbuf);
457
458                                 strcpy(cmdbuf, "");
459
460                         }
461                 /* now check the queue for new incoming stuff */
462
463                 if (CC->fake_username[0])
464                         un = CC->fake_username;
465                 else
466                         un = CC->curr_user;
467                 if (ChatLastMsg > MyLastMsg) {
468                         ThisLastMsg = ChatLastMsg;
469                         for (clptr = ChatQueue; clptr != NULL; clptr = clptr->next) {
470                                 if ((clptr->chat_seq > MyLastMsg) && ((!clptr->chat_username[0]) || (!strncasecmp(un, clptr->chat_username, 32)))) {
471                                         if ((!clptr->chat_room[0]) || (!strncasecmp(CC->room.QRname, clptr->chat_room, ROOMNAMELEN))) {
472                                                 /* Output new chat data */
473                                                 cprintf("%s\n", clptr->chat_text);
474
475                                                 /* See if we've been force-quitted (kicked etc.) */
476                                                 if (!strcmp(&clptr->chat_text[2], KICKEDMSG)) {
477                                                         allwrite("<kicked out of this room>", 0, NULL);
478                                                         cprintf("000\n");
479                                                         CC->cs_flags = CC->cs_flags - CS_CHAT;
480
481                                                         /* Kick user out of room */
482                                                         CtdlInvtKick(CC->user.fullname, 0);
483
484                                                         /* And return to the Lobby */
485                                                         usergoto(config.c_baseroom, 0, 0, NULL, NULL);
486                                                         return;
487                                                 }
488                                         }
489                                 }
490                         }
491                         MyLastMsg = ThisLastMsg;
492                 }
493         }
494 }
495
496
497
498 /*
499  * Delete any remaining instant messages
500  */
501 void delete_instant_messages(void) {
502         struct ExpressMessage *ptr;
503
504         begin_critical_section(S_SESSION_TABLE);
505         while (CC->FirstExpressMessage != NULL) {
506                 ptr = CC->FirstExpressMessage->next;
507                 if (CC->FirstExpressMessage->text != NULL)
508                         free(CC->FirstExpressMessage->text);
509                 free(CC->FirstExpressMessage);
510                 CC->FirstExpressMessage = ptr;
511         }
512         end_critical_section(S_SESSION_TABLE);
513 }
514
515
516
517
518 /*
519  * Poll for instant messages (OLD METHOD -- ***DEPRECATED ***)
520  */
521 void cmd_pexp(char *argbuf)
522 {
523         struct ExpressMessage *ptr, *holdptr;
524
525         if (CC->FirstExpressMessage == NULL) {
526                 cprintf("%d No instant messages waiting.\n", ERROR + MESSAGE_NOT_FOUND);
527                 return;
528         }
529         begin_critical_section(S_SESSION_TABLE);
530         ptr = CC->FirstExpressMessage;
531         CC->FirstExpressMessage = NULL;
532         end_critical_section(S_SESSION_TABLE);
533
534         cprintf("%d Express msgs:\n", LISTING_FOLLOWS);
535         while (ptr != NULL) {
536                 if (ptr->flags && EM_BROADCAST)
537                         cprintf("Broadcast message ");
538                 else if (ptr->flags && EM_CHAT)
539                         cprintf("Chat request ");
540                 else if (ptr->flags && EM_GO_AWAY)
541                         cprintf("Please logoff now, as requested ");
542                 else
543                         cprintf("Message ");
544                 cprintf("from %s:\n", ptr->sender);
545                 if (ptr->text != NULL)
546                         memfmout(ptr->text, 0, "\n");
547
548                 holdptr = ptr->next;
549                 if (ptr->text != NULL) free(ptr->text);
550                 free(ptr);
551                 ptr = holdptr;
552         }
553         cprintf("000\n");
554 }
555
556
557 /*
558  * Get instant messages (new method)
559  */
560 void cmd_gexp(char *argbuf) {
561         struct ExpressMessage *ptr;
562
563         if (CC->FirstExpressMessage == NULL) {
564                 cprintf("%d No instant messages waiting.\n", ERROR + MESSAGE_NOT_FOUND);
565                 return;
566         }
567
568         begin_critical_section(S_SESSION_TABLE);
569         ptr = CC->FirstExpressMessage;
570         CC->FirstExpressMessage = CC->FirstExpressMessage->next;
571         end_critical_section(S_SESSION_TABLE);
572
573         cprintf("%d %d|%ld|%d|%s|%s|%s\n",
574                 LISTING_FOLLOWS,
575                 ((ptr->next != NULL) ? 1 : 0),          /* more msgs? */
576                 (long)ptr->timestamp,                   /* time sent */
577                 ptr->flags,                             /* flags */
578                 ptr->sender,                            /* sender of msg */
579                 config.c_nodename,                      /* static for now (and possibly deprecated) */
580                 ptr->sender_email                       /* email or jid of sender */
581         );
582
583         if (ptr->text != NULL) {
584                 memfmout(ptr->text, 0, "\n");
585                 if (ptr->text[strlen(ptr->text)-1] != '\n') cprintf("\n");
586                 free(ptr->text);
587         }
588
589         cprintf("000\n");
590         free(ptr);
591 }
592
593 /*
594  * Asynchronously deliver instant messages
595  */
596 void cmd_gexp_async(void) {
597
598         /* Only do this if the session can handle asynchronous protocol */
599         if (CC->is_async == 0) return;
600
601         /* And don't do it if there's nothing to send. */
602         if (CC->FirstExpressMessage == NULL) return;
603
604         cprintf("%d instant msg\n", ASYNC_MSG + ASYNC_GEXP);
605 }
606
607 /*
608  * Back end support function for send_instant_message() and company
609  */
610 void add_xmsg_to_context(struct CitContext *ccptr, struct ExpressMessage *newmsg) 
611 {
612         struct ExpressMessage *findend;
613
614         if (ccptr->FirstExpressMessage == NULL) {
615                 ccptr->FirstExpressMessage = newmsg;
616         }
617         else {
618                 findend = ccptr->FirstExpressMessage;
619                 while (findend->next != NULL) {
620                         findend = findend->next;
621                 }
622                 findend->next = newmsg;
623         }
624
625         /* If the target context is a session which can handle asynchronous
626          * messages, go ahead and set the flag for that.
627          */
628         if (ccptr->is_async) {
629                 ccptr->async_waiting = 1;
630                 if (ccptr->state == CON_IDLE) {
631                         ccptr->state = CON_READY;
632                 }
633         }
634 }
635
636
637
638
639 /* 
640  * This is the back end to the instant message sending function.  
641  * Returns the number of users to which the message was sent.
642  * Sending a zero-length message tests for recipients without sending messages.
643  */
644 int send_instant_message(char *lun, char *lem, char *x_user, char *x_msg)
645 {
646         int message_sent = 0;           /* number of successful sends */
647         struct CitContext *ccptr;
648         struct ExpressMessage *newmsg = NULL;
649         char *un;
650         size_t msglen = 0;
651         int do_send = 0;                /* 1 = send message; 0 = only check for valid recipient */
652         static int serial_number = 0;   /* this keeps messages from getting logged twice */
653
654         if (strlen(x_msg) > 0) {
655                 msglen = strlen(x_msg) + 4;
656                 do_send = 1;
657         }
658
659         /* find the target user's context and append the message */
660         begin_critical_section(S_SESSION_TABLE);
661         ++serial_number;
662         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
663
664                 if (ccptr->fake_username[0]) {
665                         un = ccptr->fake_username;
666                 }
667                 else {
668                         un = ccptr->user.fullname;
669                 }
670
671                 if ( ((!strcasecmp(un, x_user))
672                     || (!strcasecmp(x_user, "broadcast")))
673                     && (ccptr->can_receive_im)
674                     && ((ccptr->disable_exp == 0)
675                     || (CC->user.axlevel >= 6)) ) {
676                         if (do_send) {
677                                 newmsg = (struct ExpressMessage *) malloc(sizeof (struct ExpressMessage));
678                                 memset(newmsg, 0, sizeof (struct ExpressMessage));
679                                 time(&(newmsg->timestamp));
680                                 safestrncpy(newmsg->sender, lun, sizeof newmsg->sender);
681                                 safestrncpy(newmsg->sender_email, lem, sizeof newmsg->sender_email);
682                                 if (!strcasecmp(x_user, "broadcast")) {
683                                         newmsg->flags |= EM_BROADCAST;
684                                 }
685                                 newmsg->text = strdup(x_msg);
686
687                                 add_xmsg_to_context(ccptr, newmsg);
688
689                                 /* and log it ... */
690                                 if (ccptr != CC) {
691                                         log_instant_message(CC, ccptr, newmsg->text, serial_number);
692                                 }
693                         }
694                         ++message_sent;
695                 }
696         }
697         end_critical_section(S_SESSION_TABLE);
698         return (message_sent);
699 }
700
701 /*
702  * send instant messages
703  */
704 void cmd_sexp(char *argbuf)
705 {
706         int message_sent = 0;
707         char x_user[USERNAME_SIZE];
708         char x_msg[1024];
709         char *lun;
710         char *lem;
711         char *x_big_msgbuf = NULL;
712
713         if ((!(CC->logged_in)) && (!(CC->internal_pgm))) {
714                 cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN);
715                 return;
716         }
717         if (CC->fake_username[0])
718                 lun = CC->fake_username;
719         else
720                 lun = CC->user.fullname;
721
722         lem = CC->cs_inet_email;
723
724         extract_token(x_user, argbuf, 0, '|', sizeof x_user);
725         extract_token(x_msg, argbuf, 1, '|', sizeof x_msg);
726
727         if (!x_user[0]) {
728                 cprintf("%d You were not previously paged.\n", ERROR + NO_SUCH_USER);
729                 return;
730         }
731         if ((!strcasecmp(x_user, "broadcast")) && (CC->user.axlevel < 6)) {
732                 cprintf("%d Higher access required to send a broadcast.\n",
733                         ERROR + HIGHER_ACCESS_REQUIRED);
734                 return;
735         }
736         /* This loop handles text-transfer pages */
737         if (!strcmp(x_msg, "-")) {
738                 message_sent = PerformXmsgHooks(lun, lem, x_user, "");
739                 if (message_sent == 0) {
740                         if (getuser(NULL, x_user))
741                                 cprintf("%d '%s' does not exist.\n",
742                                                 ERROR + NO_SUCH_USER, x_user);
743                         else
744                                 cprintf("%d '%s' is not logged in "
745                                                 "or is not accepting pages.\n",
746                                                 ERROR + RESOURCE_NOT_OPEN, x_user);
747                         return;
748                 }
749                 unbuffer_output();
750                 cprintf("%d Transmit message (will deliver to %d users)\n",
751                         SEND_LISTING, message_sent);
752                 x_big_msgbuf = malloc(SIZ);
753                 memset(x_big_msgbuf, 0, SIZ);
754                 while (client_getln(x_msg, sizeof x_msg) >= 0 && strcmp(x_msg, "000")) {
755                         x_big_msgbuf = realloc(x_big_msgbuf,
756                                strlen(x_big_msgbuf) + strlen(x_msg) + 4);
757                         if (!IsEmptyStr(x_big_msgbuf))
758                            if (x_big_msgbuf[strlen(x_big_msgbuf)] != '\n')
759                                 strcat(x_big_msgbuf, "\n");
760                         strcat(x_big_msgbuf, x_msg);
761                 }
762                 PerformXmsgHooks(lun, lem, x_user, x_big_msgbuf);
763                 free(x_big_msgbuf);
764
765                 /* This loop handles inline pages */
766         } else {
767                 message_sent = PerformXmsgHooks(lun, lem, x_user, x_msg);
768
769                 if (message_sent > 0) {
770                         if (!IsEmptyStr(x_msg))
771                                 cprintf("%d Message sent", CIT_OK);
772                         else
773                                 cprintf("%d Ok to send message", CIT_OK);
774                         if (message_sent > 1)
775                                 cprintf(" to %d users", message_sent);
776                         cprintf(".\n");
777                 } else {
778                         if (getuser(NULL, x_user))
779                                 cprintf("%d '%s' does not exist.\n",
780                                                 ERROR + NO_SUCH_USER, x_user);
781                         else
782                                 cprintf("%d '%s' is not logged in "
783                                                 "or is not accepting pages.\n",
784                                                 ERROR + RESOURCE_NOT_OPEN, x_user);
785                 }
786
787
788         }
789 }
790
791
792
793 /*
794  * Enter or exit paging-disabled mode
795  */
796 void cmd_dexp(char *argbuf)
797 {
798         int new_state;
799
800         if (CtdlAccessCheck(ac_logged_in)) return;
801
802         new_state = extract_int(argbuf, 0);
803         if ((new_state == 0) || (new_state == 1)) {
804                 CC->disable_exp = new_state;
805         }
806
807         cprintf("%d %d\n", CIT_OK, CC->disable_exp);
808 }
809
810
811 /*
812  * Request client termination
813  */
814 void cmd_reqt(char *argbuf) {
815         struct CitContext *ccptr;
816         int sessions = 0;
817         int which_session;
818         struct ExpressMessage *newmsg;
819
820         if (CtdlAccessCheck(ac_aide)) return;
821         which_session = extract_int(argbuf, 0);
822
823         begin_critical_section(S_SESSION_TABLE);
824         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
825                 if ((ccptr->cs_pid == which_session) || (which_session == 0)) {
826
827                         newmsg = (struct ExpressMessage *)
828                                 malloc(sizeof (struct ExpressMessage));
829                         memset(newmsg, 0,
830                                 sizeof (struct ExpressMessage));
831                         time(&(newmsg->timestamp));
832                         safestrncpy(newmsg->sender, CC->user.fullname,
833                                     sizeof newmsg->sender);
834                         newmsg->flags |= EM_GO_AWAY;
835                         newmsg->text = strdup("Automatic logoff requested.");
836
837                         add_xmsg_to_context(ccptr, newmsg);
838                         ++sessions;
839
840                 }
841         }
842         end_critical_section(S_SESSION_TABLE);
843         cprintf("%d Sent termination request to %d sessions.\n", CIT_OK, sessions);
844 }
845
846
847 /*
848  * This is the back end for flush_conversations_to_disk()
849  * At this point we've isolated a single conversation (struct imlog)
850  * and are ready to write it to disk.
851  */
852 void flush_individual_conversation(struct imlog *im) {
853         struct CtdlMessage *msg;
854         long msgnum = 0;
855         char roomname[ROOMNAMELEN];
856
857         StrBufAppendBufPlain(im->conversation, HKEY(
858                 "</body>\r\n"
859                 "</html>\r\n"
860                 ), 0
861         );
862
863         msg = malloc(sizeof(struct CtdlMessage));
864         memset(msg, 0, sizeof(struct CtdlMessage));
865         msg->cm_magic = CTDLMESSAGE_MAGIC;
866         msg->cm_anon_type = MES_NORMAL;
867         msg->cm_format_type = FMT_RFC822;
868         if (!IsEmptyStr(im->usernames[0])) {
869                 msg->cm_fields['A'] = strdup(im->usernames[0]);
870         } else {
871                 msg->cm_fields['A'] = strdup("Citadel");
872         }
873         if (!IsEmptyStr(im->usernames[1])) {
874                 msg->cm_fields['R'] = strdup(im->usernames[1]);
875         }
876         msg->cm_fields['O'] = strdup(PAGELOGROOM);
877         msg->cm_fields['N'] = strdup(NODENAME);
878         msg->cm_fields['M'] = SmashStrBuf(&im->conversation);   /* we own this memory now */
879
880         /* Start with usernums[1] because it's guaranteed to be higher than usernums[0],
881          * so if there's only one party, usernums[0] will be zero but usernums[1] won't.
882          * Create the room if necessary.  Note that we create as a type 5 room rather
883          * than 4, which indicates that it's a personal room but we've already supplied
884          * the namespace prefix.
885          *
886          * In the unlikely event that usernums[1] is zero, a room with an invalid namespace
887          * prefix will be created.  That's ok because the auto-purger will clean it up later.
888          */
889         snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[1], PAGELOGROOM);
890         create_room(roomname, 5, "", 0, 1, 1, VIEW_BBS);
891         msgnum = CtdlSubmitMsg(msg, NULL, roomname, 0);
892         CtdlFreeMessage(msg);
893
894         /* If there is a valid user number in usernums[0], save a copy for them too. */
895         if (im->usernums[0] > 0) {
896                 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[0], PAGELOGROOM);
897                 create_room(roomname, 5, "", 0, 1, 1, VIEW_BBS);
898                 CtdlSaveMsgPointerInRoom(roomname, msgnum, 0, NULL);
899         }
900
901         /* Finally, if we're logging instant messages globally, do that now. */
902         if (!IsEmptyStr(config.c_logpages)) {
903                 create_room(config.c_logpages, 3, "", 0, 1, 1, VIEW_BBS);
904                 CtdlSaveMsgPointerInRoom(config.c_logpages, msgnum, 0, NULL);
905         }
906
907 }
908
909 /*
910  * Locate instant message conversations which have gone idle
911  * (or, if the server is shutting down, locate *all* conversations)
912  * and flush them to disk (in the participants' log rooms, etc.)
913  */
914 void flush_conversations_to_disk(time_t if_older_than) {
915
916         struct imlog *flush_these = NULL;
917         struct imlog *dont_flush_these = NULL;
918         struct imlog *imptr = NULL;
919
920         begin_critical_section(S_IM_LOGS);
921         while (imlist)
922         {
923                 imptr = imlist;
924                 imlist = imlist->next;
925                 if ((time(NULL) - imptr->lastmsg) > if_older_than)
926                 {
927                         /* This conversation qualifies.  Move it to the list of ones to flush. */
928                         imptr->next = flush_these;
929                         flush_these = imptr;
930                 }
931                 else  {
932                         /* Move it to the list of ones not to flush. */
933                         imptr->next = dont_flush_these;
934                         dont_flush_these = imptr;
935                 }
936         }
937         imlist = dont_flush_these;
938         end_critical_section(S_IM_LOGS);
939
940         /* We are now outside of the critical section, and we are the only thread holding a
941          * pointer to a linked list of conversations to be flushed to disk.
942          */
943         while (flush_these) {
944
945                 flush_individual_conversation(flush_these);     /* This will free the string buffer */
946                 imptr = flush_these;
947                 flush_these = flush_these->next;
948                 free(imptr);
949         }
950 }
951
952
953
954 void chat_timer(void) {
955         flush_conversations_to_disk(300);       /* Anything that hasn't peeped in more than 5 minutes */
956 }
957
958 void chat_shutdown(void) {
959         flush_conversations_to_disk(0);         /* Get it ALL onto disk NOW. */
960 }
961
962 CTDL_MODULE_INIT(chat)
963 {
964         if (!threading)
965         {
966                 CtdlRegisterProtoHook(cmd_chat, "CHAT", "Begin real-time chat");
967                 CtdlRegisterProtoHook(cmd_pexp, "PEXP", "Poll for instant messages");
968                 CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
969                 CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
970                 CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
971                 CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
972                 CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC);
973                 CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP);
974                 CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
975                 CtdlRegisterSessionHook(chat_timer, EVT_TIMER);
976                 CtdlRegisterSessionHook(chat_shutdown, EVT_SHUTDOWN);
977         }
978         
979         /* return our Subversion id for the Log */
980         return "$Id$";
981 }