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