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