* remove replace part of memfmout(); its not used anymore anyways
[citadel.git] / citadel / modules / chat / serv_chat.c
index fff6640fe55f9156af8f546238e2c0202ec48adf..f0f1e7d6c40f4a73d976372cf27f75cf9c28628d 100644 (file)
@@ -4,6 +4,22 @@
  * This module handles all "real time" communication between users.  The
  * modes of communication currently supported are Chat and Paging.
  *
+ * Copyright (c) 1987-2009 by the citadel.org team
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
  */
 #include "sysdep.h"
 #include <stdlib.h>
 #include <sys/wait.h>
 #include <string.h>
 #include <limits.h>
+#include <libcitadel.h>
 #include "citadel.h"
 #include "server.h"
 #include "serv_chat.h"
 #include "citserver.h"
 #include "support.h"
 #include "config.h"
-#include "tools.h"
 #include "msgbase.h"
 #include "user_ops.h"
-#include "room_ops.h"
 
 #ifndef HAVE_SNPRINTF
 #include "snprintf.h"
 #endif
 
-
 #include "ctdl_module.h"
 
-
-
 struct ChatLine *ChatQueue = NULL;
 int ChatLastMsg = 0;
 
+struct imlog {
+       struct imlog *next;
+       long usernums[2];
+       char usernames[2][128];
+       time_t lastmsg;
+       int last_serial;
+       StrBuf *conversation;
+};
+
+struct imlog *imlist = NULL;
+
+
+
+
+/*
+ * FIXME: OMG this module is realy horrible to the rest of the system when accessing contexts.
+ * It pays no regard at all to how long it may have the context list locked for. 
+ * It carries out IO whilst the context list is locked.
+ * I'd recomend disabling this module altogether for the moment.
+ */
+
+/*
+ * This function handles the logging of instant messages to disk.
+ */
+void log_instant_message(struct CitContext *me, struct CitContext *them, char *msgtext, int serial_number)
+{
+       long usernums[2];
+       long t;
+       struct imlog *iptr = NULL;
+       struct imlog *this_im = NULL;
+       
+       memset(usernums, 0, sizeof usernums);
+       usernums[0] = me->user.usernum;
+       usernums[1] = them->user.usernum;
+
+       /* Always put the lower user number first, so we can use the array as a hash value which
+        * represents a pair of users.  For a broadcast message one of the users will be 0.
+        */
+       if (usernums[0] > usernums[1]) {
+               t = usernums[0];
+               usernums[0] = usernums[1];
+               usernums[1] = t;
+       }
+
+       begin_critical_section(S_IM_LOGS);
+
+       /* Look for an existing conversation in the hash table.
+        * If not found, create a new one.
+        */
+
+       this_im = NULL;
+       for (iptr = imlist; iptr != NULL; iptr = iptr->next) {
+               if ((iptr->usernums[0] == usernums[0]) && (iptr->usernums[1] == usernums[1])) {
+                       /* Existing conversation */
+                       this_im = iptr;
+               }
+       }
+       if (this_im == NULL) {
+               /* New conversation */
+               this_im = malloc(sizeof(struct imlog));
+               memset(this_im, 0, sizeof (struct imlog));
+               this_im->usernums[0] = usernums[0];
+               this_im->usernums[1] = usernums[1];
+               /* usernames[] and usernums[] might not be in the same order.  This is not an error. */
+               if (me) {
+                       safestrncpy(this_im->usernames[0], me->user.fullname, sizeof this_im->usernames[0]);
+               }
+               if (them) {
+                       safestrncpy(this_im->usernames[1], them->user.fullname, sizeof this_im->usernames[1]);
+               }
+               this_im->conversation = NewStrBuf();
+               this_im->next = imlist;
+               imlist = this_im;
+               StrBufAppendBufPlain(this_im->conversation, HKEY(
+                       "Content-type: text/html\r\n"
+                       "Content-transfer-encoding: 7bit\r\n"
+                       "\r\n"
+                       "<html><body>\r\n"
+                       ), 0);
+       }
+
+
+       /* Since it's possible for this function to get called more than once if a user is logged
+        * in on multiple sessions, we use the message's serial number to keep track of whether
+        * we've already logged it.
+        */
+       if (this_im->last_serial != serial_number)
+       {
+               this_im->lastmsg = time(NULL);          /* Touch the timestamp so we know when to flush */
+               this_im->last_serial = serial_number;
+               StrBufAppendBufPlain(this_im->conversation, HKEY("<p><b>"), 0);
+               StrBufAppendBufPlain(this_im->conversation, me->user.fullname, -1, 0);
+               StrBufAppendBufPlain(this_im->conversation, HKEY(":</b> "), 0);
+               StrEscAppend(this_im->conversation, NULL, msgtext, 0, 0);
+               StrBufAppendBufPlain(this_im->conversation, HKEY("</p>\r\n"), 0);
+       }
+       end_critical_section(S_IM_LOGS);
+}
+
 /*
  * This message can be set to anything you want, but it is
  * checked for consistency so don't move it away from here.
@@ -129,9 +240,9 @@ void allwrite(char *cmdbuf, int flag, char *username)
 }
 
 
-t_context *find_context(char **unstr)
+CitContext *find_context(char **unstr)
 {
-       t_context *t_cc, *found_cc = NULL;
+       CitContext *t_cc, *found_cc = NULL;
        char *name, *tptr;
 
        if ((!*unstr) || (!unstr))
@@ -183,13 +294,11 @@ void do_chat_listing(int allflag)
                }
 
                GenerateRoomDisplay(roomname, ccptr, CC);
-               if ((CC->user.axlevel < 6)
-                  && (!IsEmptyStr(ccptr->fake_roomname))) {
+               if ((CC->user.axlevel < 6) && (!IsEmptyStr(ccptr->fake_roomname))) {
                        strcpy(roomname, ccptr->fake_roomname);
                }
 
-               if ((ccptr->cs_flags & CS_CHAT)
-                   && ((ccptr->cs_flags & CS_STEALTH) == 0)) {
+               if ((ccptr->cs_flags & CS_CHAT) && ((ccptr->cs_flags & CS_STEALTH) == 0)) {
                        if ((allflag == 0) || (allflag == 1)) {
                                cprintf(":| %-25s <%s>:\n",
                                        (ccptr->fake_username[0]) ? ccptr->fake_username : ccptr->curr_user,
@@ -398,7 +507,7 @@ void cmd_chat(char *argbuf)
                                                        CtdlInvtKick(CC->user.fullname, 0);
 
                                                        /* And return to the Lobby */
-                                                       usergoto(config.c_baseroom, 0, 0, NULL, NULL);
+                                                       CtdlUserGoto(config.c_baseroom, 0, 0, NULL, NULL);
                                                        return;
                                                }
                                        }
@@ -424,9 +533,9 @@ void delete_instant_messages(void) {
                        free(CC->FirstExpressMessage->text);
                free(CC->FirstExpressMessage);
                CC->FirstExpressMessage = ptr;
-               }
-       end_critical_section(S_SESSION_TABLE);
        }
+       end_critical_section(S_SESSION_TABLE);
+}
 
 
 
@@ -459,7 +568,7 @@ void cmd_pexp(char *argbuf)
                        cprintf("Message ");
                cprintf("from %s:\n", ptr->sender);
                if (ptr->text != NULL)
-                       memfmout(ptr->text, 0, "\n");
+                       memfmout(ptr->text, "\n");
 
                holdptr = ptr->next;
                if (ptr->text != NULL) free(ptr->text);
@@ -486,17 +595,18 @@ void cmd_gexp(char *argbuf) {
        CC->FirstExpressMessage = CC->FirstExpressMessage->next;
        end_critical_section(S_SESSION_TABLE);
 
-       cprintf("%d %d|%ld|%d|%s|%s\n",
+       cprintf("%d %d|%ld|%d|%s|%s|%s\n",
                LISTING_FOLLOWS,
                ((ptr->next != NULL) ? 1 : 0),          /* more msgs? */
                (long)ptr->timestamp,                   /* time sent */
                ptr->flags,                             /* flags */
                ptr->sender,                            /* sender of msg */
-               config.c_nodename                       /* static for now */
+               config.c_nodename,                      /* static for now (and possibly deprecated) */
+               ptr->sender_email                       /* email or jid of sender */
        );
 
        if (ptr->text != NULL) {
-               memfmout(ptr->text, 0, "\n");
+               memfmout(ptr->text, "\n");
                if (ptr->text[strlen(ptr->text)-1] != '\n') cprintf("\n");
                free(ptr->text);
        }
@@ -522,8 +632,7 @@ void cmd_gexp_async(void) {
 /*
  * Back end support function for send_instant_message() and company
  */
-void add_xmsg_to_context(struct CitContext *ccptr, 
-                       struct ExpressMessage *newmsg) 
+void add_xmsg_to_context(struct CitContext *ccptr, struct ExpressMessage *newmsg) 
 {
        struct ExpressMessage *findend;
 
@@ -557,20 +666,15 @@ void add_xmsg_to_context(struct CitContext *ccptr,
  * Returns the number of users to which the message was sent.
  * Sending a zero-length message tests for recipients without sending messages.
  */
-int send_instant_message(char *lun, char *x_user, char *x_msg)
+int send_instant_message(char *lun, char *lem, char *x_user, char *x_msg)
 {
        int message_sent = 0;           /* number of successful sends */
        struct CitContext *ccptr;
-       struct ExpressMessage *newmsg;
+       struct ExpressMessage *newmsg = NULL;
        char *un;
        size_t msglen = 0;
-       int do_send = 0;                /* set to 1 to actually page, not
-                                        * just check to see if we can.
-                                        */
-       struct savelist *sl = NULL;     /* list of rooms to save this page */
-       struct savelist *sptr;
-       struct CtdlMessage *logmsg = NULL;
-       long msgnum;
+       int do_send = 0;                /* 1 = send message; 0 = only check for valid recipient */
+       static int serial_number = 0;   /* this keeps messages from getting logged twice */
 
        if (strlen(x_msg) > 0) {
                msglen = strlen(x_msg) + 4;
@@ -579,6 +683,7 @@ int send_instant_message(char *lun, char *x_user, char *x_msg)
 
        /* find the target user's context and append the message */
        begin_critical_section(S_SESSION_TABLE);
+       ++serial_number;
        for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
 
                if (ccptr->fake_username[0]) {
@@ -590,81 +695,31 @@ int send_instant_message(char *lun, char *x_user, char *x_msg)
 
                if ( ((!strcasecmp(un, x_user))
                    || (!strcasecmp(x_user, "broadcast")))
+                   && (ccptr->can_receive_im)
                    && ((ccptr->disable_exp == 0)
                    || (CC->user.axlevel >= 6)) ) {
                        if (do_send) {
-                               newmsg = (struct ExpressMessage *)
-                                       malloc(sizeof (struct ExpressMessage));
-                               memset(newmsg, 0,
-                                       sizeof (struct ExpressMessage));
+                               newmsg = (struct ExpressMessage *) malloc(sizeof (struct ExpressMessage));
+                               memset(newmsg, 0, sizeof (struct ExpressMessage));
                                time(&(newmsg->timestamp));
-                               safestrncpy(newmsg->sender, lun,
-                                           sizeof newmsg->sender);
-                               if (!strcasecmp(x_user, "broadcast"))
+                               safestrncpy(newmsg->sender, lun, sizeof newmsg->sender);
+                               safestrncpy(newmsg->sender_email, lem, sizeof newmsg->sender_email);
+                               if (!strcasecmp(x_user, "broadcast")) {
                                        newmsg->flags |= EM_BROADCAST;
+                               }
                                newmsg->text = strdup(x_msg);
 
                                add_xmsg_to_context(ccptr, newmsg);
 
                                /* and log it ... */
                                if (ccptr != CC) {
-                                       sptr = (struct savelist *)
-                                               malloc(sizeof(struct savelist));
-                                       sptr->next = sl;
-                                       MailboxName(sptr->roomname,
-                                                   sizeof sptr->roomname,
-                                               &ccptr->user, PAGELOGROOM);
-                                       sl = sptr;
+                                       log_instant_message(CC, ccptr, newmsg->text, serial_number);
                                }
                        }
                        ++message_sent;
                }
        }
        end_critical_section(S_SESSION_TABLE);
-
-       /* Log the page to disk if configured to do so  */
-       if ( (do_send) && (message_sent) ) {
-
-               logmsg = malloc(sizeof(struct CtdlMessage));
-               memset(logmsg, 0, sizeof(struct CtdlMessage));
-               logmsg->cm_magic = CTDLMESSAGE_MAGIC;
-               logmsg->cm_anon_type = MES_NORMAL;
-               logmsg->cm_format_type = 0;
-               logmsg->cm_fields['A'] = strdup(lun);
-               logmsg->cm_fields['N'] = strdup(NODENAME);
-               logmsg->cm_fields['O'] = strdup(PAGELOGROOM);
-               logmsg->cm_fields['R'] = strdup(x_user);
-               logmsg->cm_fields['M'] = strdup(x_msg);
-
-
-               /* Save a copy of the message in the sender's log room,
-                * creating the room if necessary.
-                */
-               create_room(PAGELOGROOM, 4, "", 0, 1, 0, VIEW_BBS);
-               msgnum = CtdlSubmitMsg(logmsg, NULL, PAGELOGROOM);
-
-               /* Now save a copy in the global log room, if configured */
-               if (!IsEmptyStr(config.c_logpages)) {
-                       create_room(config.c_logpages, 3, "", 0, 1, 1, VIEW_BBS);
-                       CtdlSaveMsgPointerInRoom(config.c_logpages, msgnum, 0, NULL);
-               }
-
-               /* Save a copy in each recipient's log room, creating those
-                * rooms if necessary.  Note that we create as a type 5 room
-                * rather than 4, which indicates that it's a personal room
-                * but we've already supplied the namespace prefix.
-                */
-               while (sl != NULL) {
-                       create_room(sl->roomname, 5, "", 0, 1, 1, VIEW_BBS);
-                       CtdlSaveMsgPointerInRoom(sl->roomname, msgnum, 0, NULL);
-                       sptr = sl->next;
-                       free(sl);
-                       sl = sptr;
-               }
-
-               CtdlFreeMessage(logmsg);
-       }
-
        return (message_sent);
 }
 
@@ -677,6 +732,7 @@ void cmd_sexp(char *argbuf)
        char x_user[USERNAME_SIZE];
        char x_msg[1024];
        char *lun;
+       char *lem;
        char *x_big_msgbuf = NULL;
 
        if ((!(CC->logged_in)) && (!(CC->internal_pgm))) {
@@ -688,6 +744,8 @@ void cmd_sexp(char *argbuf)
        else
                lun = CC->user.fullname;
 
+       lem = CC->cs_inet_email;
+
        extract_token(x_user, argbuf, 0, '|', sizeof x_user);
        extract_token(x_msg, argbuf, 1, '|', sizeof x_msg);
 
@@ -702,9 +760,9 @@ void cmd_sexp(char *argbuf)
        }
        /* This loop handles text-transfer pages */
        if (!strcmp(x_msg, "-")) {
-               message_sent = PerformXmsgHooks(lun, x_user, "");
+               message_sent = PerformXmsgHooks(lun, lem, x_user, "");
                if (message_sent == 0) {
-                       if (getuser(NULL, x_user))
+                       if (CtdlGetUser(NULL, x_user))
                                cprintf("%d '%s' does not exist.\n",
                                                ERROR + NO_SUCH_USER, x_user);
                        else
@@ -718,8 +776,7 @@ void cmd_sexp(char *argbuf)
                        SEND_LISTING, message_sent);
                x_big_msgbuf = malloc(SIZ);
                memset(x_big_msgbuf, 0, SIZ);
-               while (client_getln(x_msg, sizeof x_msg),
-                     strcmp(x_msg, "000")) {
+               while (client_getln(x_msg, sizeof x_msg) >= 0 && strcmp(x_msg, "000")) {
                        x_big_msgbuf = realloc(x_big_msgbuf,
                               strlen(x_big_msgbuf) + strlen(x_msg) + 4);
                        if (!IsEmptyStr(x_big_msgbuf))
@@ -727,12 +784,12 @@ void cmd_sexp(char *argbuf)
                                strcat(x_big_msgbuf, "\n");
                        strcat(x_big_msgbuf, x_msg);
                }
-               PerformXmsgHooks(lun, x_user, x_big_msgbuf);
+               PerformXmsgHooks(lun, lem, x_user, x_big_msgbuf);
                free(x_big_msgbuf);
 
                /* This loop handles inline pages */
        } else {
-               message_sent = PerformXmsgHooks(lun, x_user, x_msg);
+               message_sent = PerformXmsgHooks(lun, lem, x_user, x_msg);
 
                if (message_sent > 0) {
                        if (!IsEmptyStr(x_msg))
@@ -743,7 +800,7 @@ void cmd_sexp(char *argbuf)
                                cprintf(" to %d users", message_sent);
                        cprintf(".\n");
                } else {
-                       if (getuser(NULL, x_user))
+                       if (CtdlGetUser(NULL, x_user))
                                cprintf("%d '%s' does not exist.\n",
                                                ERROR + NO_SUCH_USER, x_user);
                        else
@@ -812,20 +869,138 @@ void cmd_reqt(char *argbuf) {
 }
 
 
+/*
+ * This is the back end for flush_conversations_to_disk()
+ * At this point we've isolated a single conversation (struct imlog)
+ * and are ready to write it to disk.
+ */
+void flush_individual_conversation(struct imlog *im) {
+       struct CtdlMessage *msg;
+       long msgnum = 0;
+       char roomname[ROOMNAMELEN];
+
+       StrBufAppendBufPlain(im->conversation, HKEY(
+               "</body>\r\n"
+               "</html>\r\n"
+               ), 0
+       );
+
+       msg = malloc(sizeof(struct CtdlMessage));
+       memset(msg, 0, sizeof(struct CtdlMessage));
+       msg->cm_magic = CTDLMESSAGE_MAGIC;
+       msg->cm_anon_type = MES_NORMAL;
+       msg->cm_format_type = FMT_RFC822;
+       if (!IsEmptyStr(im->usernames[0])) {
+               msg->cm_fields['A'] = strdup(im->usernames[0]);
+       } else {
+               msg->cm_fields['A'] = strdup("Citadel");
+       }
+       if (!IsEmptyStr(im->usernames[1])) {
+               msg->cm_fields['R'] = strdup(im->usernames[1]);
+       }
+       msg->cm_fields['O'] = strdup(PAGELOGROOM);
+       msg->cm_fields['N'] = strdup(NODENAME);
+       msg->cm_fields['M'] = SmashStrBuf(&im->conversation);   /* we own this memory now */
+
+       /* Start with usernums[1] because it's guaranteed to be higher than usernums[0],
+        * so if there's only one party, usernums[0] will be zero but usernums[1] won't.
+        * Create the room if necessary.  Note that we create as a type 5 room rather
+        * than 4, which indicates that it's a personal room but we've already supplied
+        * the namespace prefix.
+        *
+        * In the unlikely event that usernums[1] is zero, a room with an invalid namespace
+        * prefix will be created.  That's ok because the auto-purger will clean it up later.
+        */
+       snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[1], PAGELOGROOM);
+       CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
+       msgnum = CtdlSubmitMsg(msg, NULL, roomname, 0);
+       CtdlFreeMessage(msg);
+
+       /* If there is a valid user number in usernums[0], save a copy for them too. */
+       if (im->usernums[0] > 0) {
+               snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[0], PAGELOGROOM);
+               CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
+               CtdlSaveMsgPointerInRoom(roomname, msgnum, 0, NULL);
+       }
+
+       /* Finally, if we're logging instant messages globally, do that now. */
+       if (!IsEmptyStr(config.c_logpages)) {
+               CtdlCreateRoom(config.c_logpages, 3, "", 0, 1, 1, VIEW_BBS);
+               CtdlSaveMsgPointerInRoom(config.c_logpages, msgnum, 0, NULL);
+       }
+
+}
+
+/*
+ * Locate instant message conversations which have gone idle
+ * (or, if the server is shutting down, locate *all* conversations)
+ * and flush them to disk (in the participants' log rooms, etc.)
+ */
+void flush_conversations_to_disk(time_t if_older_than) {
+
+       struct imlog *flush_these = NULL;
+       struct imlog *dont_flush_these = NULL;
+       struct imlog *imptr = NULL;
+
+       begin_critical_section(S_IM_LOGS);
+       while (imlist)
+       {
+               imptr = imlist;
+               imlist = imlist->next;
+               if ((time(NULL) - imptr->lastmsg) > if_older_than)
+               {
+                       /* This conversation qualifies.  Move it to the list of ones to flush. */
+                       imptr->next = flush_these;
+                       flush_these = imptr;
+               }
+               else  {
+                       /* Move it to the list of ones not to flush. */
+                       imptr->next = dont_flush_these;
+                       dont_flush_these = imptr;
+               }
+       }
+       imlist = dont_flush_these;
+       end_critical_section(S_IM_LOGS);
+
+       /* We are now outside of the critical section, and we are the only thread holding a
+        * pointer to a linked list of conversations to be flushed to disk.
+        */
+       while (flush_these) {
+
+               flush_individual_conversation(flush_these);     /* This will free the string buffer */
+               imptr = flush_these;
+               flush_these = flush_these->next;
+               free(imptr);
+       }
+}
+
+
+
+void chat_timer(void) {
+       flush_conversations_to_disk(300);       /* Anything that hasn't peeped in more than 5 minutes */
+}
+
+void chat_shutdown(void) {
+       flush_conversations_to_disk(0);         /* Get it ALL onto disk NOW. */
+}
 
 CTDL_MODULE_INIT(chat)
 {
-       CtdlRegisterProtoHook(cmd_chat, "CHAT", "Begin real-time chat");
-       CtdlRegisterProtoHook(cmd_pexp, "PEXP", "Poll for instant messages");
-       CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
-       CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
-       CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
-       CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
-       CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC);
-       CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP);
-       CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
-
+       if (!threading)
+       {
+               CtdlRegisterProtoHook(cmd_chat, "CHAT", "Begin real-time chat");
+               CtdlRegisterProtoHook(cmd_pexp, "PEXP", "Poll for instant messages");
+               CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
+               CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
+               CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
+               CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
+               CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC);
+               CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP);
+               CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
+               CtdlRegisterSessionHook(chat_timer, EVT_TIMER);
+               CtdlRegisterSessionHook(chat_shutdown, EVT_SHUTDOWN);
+       }
+       
        /* return our Subversion id for the Log */
        return "$Id$";
 }
-