Back to index

citadel  8.12
serv_instmsg.c
Go to the documentation of this file.
00001 /*
00002  * This module handles instant messaging between users.
00003  * 
00004  * Copyright (c) 1987-2012 by the citadel.org team
00005  *
00006  * This program is open source software; you can redistribute it and/or modify
00007  * it under the terms of the GNU General Public License version 3.
00008  * 
00009  * 
00010  *
00011  * This program is distributed in the hope that it will be useful,
00012  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00013  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014  * GNU General Public License for more details.
00015  *
00016  * 
00017  * 
00018  * 
00019  *
00020  */
00021 #include "sysdep.h"
00022 #include <stdlib.h>
00023 #include <unistd.h>
00024 #include <stdio.h>
00025 #include <fcntl.h>
00026 #include <signal.h>
00027 #include <pwd.h>
00028 #include <errno.h>
00029 #include <sys/types.h>
00030 
00031 #if TIME_WITH_SYS_TIME
00032 # include <sys/time.h>
00033 # include <time.h>
00034 #else
00035 # if HAVE_SYS_TIME_H
00036 #  include <sys/time.h>
00037 # else
00038 #  include <time.h>
00039 # endif
00040 #endif
00041 
00042 #include <sys/wait.h>
00043 #include <string.h>
00044 #include <limits.h>
00045 #include <libcitadel.h>
00046 #include "citadel.h"
00047 #include "server.h"
00048 #include "serv_instmsg.h"
00049 #include "citserver.h"
00050 #include "support.h"
00051 #include "config.h"
00052 #include "msgbase.h"
00053 #include "user_ops.h"
00054 
00055 #ifndef HAVE_SNPRINTF
00056 #include "snprintf.h"
00057 #endif
00058 
00059 #include "ctdl_module.h"
00060 
00061 struct imlog {
00062        struct imlog *next;
00063        long usernums[2];
00064        char usernames[2][128];
00065        time_t lastmsg;
00066        int last_serial;
00067        StrBuf *conversation;
00068 };
00069 
00070 struct imlog *imlist = NULL;
00071 
00072 /*
00073  * This function handles the logging of instant messages to disk.
00074  */
00075 void log_instant_message(struct CitContext *me, struct CitContext *them, char *msgtext, int serial_number)
00076 {
00077        long usernums[2];
00078        long t;
00079        struct imlog *iptr = NULL;
00080        struct imlog *this_im = NULL;
00081        
00082        memset(usernums, 0, sizeof usernums);
00083        usernums[0] = me->user.usernum;
00084        usernums[1] = them->user.usernum;
00085 
00086        /* Always put the lower user number first, so we can use the array as a hash value which
00087         * represents a pair of users.  For a broadcast message one of the users will be 0.
00088         */
00089        if (usernums[0] > usernums[1]) {
00090               t = usernums[0];
00091               usernums[0] = usernums[1];
00092               usernums[1] = t;
00093        }
00094 
00095        begin_critical_section(S_IM_LOGS);
00096 
00097        /* Look for an existing conversation in the hash table.
00098         * If not found, create a new one.
00099         */
00100 
00101        this_im = NULL;
00102        for (iptr = imlist; iptr != NULL; iptr = iptr->next) {
00103               if ((iptr->usernums[0] == usernums[0]) && (iptr->usernums[1] == usernums[1])) {
00104                      /* Existing conversation */
00105                      this_im = iptr;
00106               }
00107        }
00108        if (this_im == NULL) {
00109               /* New conversation */
00110               this_im = malloc(sizeof(struct imlog));
00111               memset(this_im, 0, sizeof (struct imlog));
00112               this_im->usernums[0] = usernums[0];
00113               this_im->usernums[1] = usernums[1];
00114               /* usernames[] and usernums[] might not be in the same order.  This is not an error. */
00115               if (me) {
00116                      safestrncpy(this_im->usernames[0], me->user.fullname, sizeof this_im->usernames[0]);
00117               }
00118               if (them) {
00119                      safestrncpy(this_im->usernames[1], them->user.fullname, sizeof this_im->usernames[1]);
00120               }
00121               this_im->conversation = NewStrBuf();
00122               this_im->next = imlist;
00123               imlist = this_im;
00124               StrBufAppendBufPlain(this_im->conversation, HKEY(
00125                      "Content-type: text/html\r\n"
00126                      "Content-transfer-encoding: 7bit\r\n"
00127                      "\r\n"
00128                      "<html><body>\r\n"
00129                      ), 0);
00130        }
00131 
00132 
00133        /* Since it's possible for this function to get called more than once if a user is logged
00134         * in on multiple sessions, we use the message's serial number to keep track of whether
00135         * we've already logged it.
00136         */
00137        if (this_im->last_serial != serial_number)
00138        {
00139               this_im->lastmsg = time(NULL);            /* Touch the timestamp so we know when to flush */
00140               this_im->last_serial = serial_number;
00141               StrBufAppendBufPlain(this_im->conversation, HKEY("<p><b>"), 0);
00142               StrBufAppendBufPlain(this_im->conversation, me->user.fullname, -1, 0);
00143               StrBufAppendBufPlain(this_im->conversation, HKEY(":</b> "), 0);
00144               StrEscAppend(this_im->conversation, NULL, msgtext, 0, 0);
00145               StrBufAppendBufPlain(this_im->conversation, HKEY("</p>\r\n"), 0);
00146        }
00147        end_critical_section(S_IM_LOGS);
00148 }
00149 
00150 
00151 /*
00152  * Delete any remaining instant messages
00153  */
00154 void delete_instant_messages(void) {
00155        struct ExpressMessage *ptr;
00156 
00157        begin_critical_section(S_SESSION_TABLE);
00158        while (CC->FirstExpressMessage != NULL) {
00159               ptr = CC->FirstExpressMessage->next;
00160               if (CC->FirstExpressMessage->text != NULL)
00161                      free(CC->FirstExpressMessage->text);
00162               free(CC->FirstExpressMessage);
00163               CC->FirstExpressMessage = ptr;
00164        }
00165        end_critical_section(S_SESSION_TABLE);
00166 }
00167 
00168 
00169 
00170 /*
00171  * Retrieve instant messages
00172  */
00173 void cmd_gexp(char *argbuf) {
00174        struct ExpressMessage *ptr;
00175 
00176        if (CC->FirstExpressMessage == NULL) {
00177               cprintf("%d No instant messages waiting.\n", ERROR + MESSAGE_NOT_FOUND);
00178               return;
00179        }
00180 
00181        begin_critical_section(S_SESSION_TABLE);
00182        ptr = CC->FirstExpressMessage;
00183        CC->FirstExpressMessage = CC->FirstExpressMessage->next;
00184        end_critical_section(S_SESSION_TABLE);
00185 
00186        cprintf("%d %d|%ld|%d|%s|%s|%s\n",
00187               LISTING_FOLLOWS,
00188               ((ptr->next != NULL) ? 1 : 0),            /* more msgs? */
00189               (long)ptr->timestamp,                     /* time sent */
00190               ptr->flags,                        /* flags */
00191               ptr->sender,                       /* sender of msg */
00192               config.c_nodename,                 /* static for now (and possibly deprecated) */
00193               ptr->sender_email                  /* email or jid of sender */
00194        );
00195 
00196        if (ptr->text != NULL) {
00197               memfmout(ptr->text, "\n");
00198               free(ptr->text);
00199        }
00200 
00201        cprintf("000\n");
00202        free(ptr);
00203 }
00204 
00205 /*
00206  * Asynchronously deliver instant messages
00207  */
00208 void cmd_gexp_async(void) {
00209 
00210        /* Only do this if the session can handle asynchronous protocol */
00211        if (CC->is_async == 0) return;
00212 
00213        /* And don't do it if there's nothing to send. */
00214        if (CC->FirstExpressMessage == NULL) return;
00215 
00216        cprintf("%d instant msg\n", ASYNC_MSG + ASYNC_GEXP);
00217 }
00218 
00219 /*
00220  * Back end support function for send_instant_message() and company
00221  */
00222 void add_xmsg_to_context(struct CitContext *ccptr, struct ExpressMessage *newmsg) 
00223 {
00224        struct ExpressMessage *findend;
00225 
00226        if (ccptr->FirstExpressMessage == NULL) {
00227               ccptr->FirstExpressMessage = newmsg;
00228        }
00229        else {
00230               findend = ccptr->FirstExpressMessage;
00231               while (findend->next != NULL) {
00232                      findend = findend->next;
00233               }
00234               findend->next = newmsg;
00235        }
00236 
00237        /* If the target context is a session which can handle asynchronous
00238         * messages, go ahead and set the flag for that.
00239         */
00240        set_async_waiting(ccptr);
00241 }
00242 
00243 
00244 
00245 
00246 /* 
00247  * This is the back end to the instant message sending function.  
00248  * Returns the number of users to which the message was sent.
00249  * Sending a zero-length message tests for recipients without sending messages.
00250  */
00251 int send_instant_message(char *lun, char *lem, char *x_user, char *x_msg)
00252 {
00253        int message_sent = 0;              /* number of successful sends */
00254        struct CitContext *ccptr;
00255        struct ExpressMessage *newmsg = NULL;
00256        char *un;
00257        int do_send = 0;            /* 1 = send message; 0 = only check for valid recipient */
00258        static int serial_number = 0;      /* this keeps messages from getting logged twice */
00259 
00260        if (strlen(x_msg) > 0) {
00261               do_send = 1;
00262        }
00263 
00264        /* find the target user's context and append the message */
00265        begin_critical_section(S_SESSION_TABLE);
00266        ++serial_number;
00267        for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
00268 
00269               if (ccptr->fake_username[0]) {
00270                      un = ccptr->fake_username;
00271               }
00272               else {
00273                      un = ccptr->user.fullname;
00274               }
00275 
00276               if ( ((!strcasecmp(un, x_user))
00277                   || (!strcasecmp(x_user, "broadcast")))
00278                   && (ccptr->can_receive_im)
00279                   && ((ccptr->disable_exp == 0)
00280                   || (CC->user.axlevel >= AxAideU)) ) {
00281                      if (do_send) {
00282                             newmsg = (struct ExpressMessage *) malloc(sizeof (struct ExpressMessage));
00283                             memset(newmsg, 0, sizeof (struct ExpressMessage));
00284                             time(&(newmsg->timestamp));
00285                             safestrncpy(newmsg->sender, lun, sizeof newmsg->sender);
00286                             safestrncpy(newmsg->sender_email, lem, sizeof newmsg->sender_email);
00287                             if (!strcasecmp(x_user, "broadcast")) {
00288                                    newmsg->flags |= EM_BROADCAST;
00289                             }
00290                             newmsg->text = strdup(x_msg);
00291 
00292                             add_xmsg_to_context(ccptr, newmsg);
00293 
00294                             /* and log it ... */
00295                             if (ccptr != CC) {
00296                                    log_instant_message(CC, ccptr, newmsg->text, serial_number);
00297                             }
00298                      }
00299                      ++message_sent;
00300               }
00301        }
00302        end_critical_section(S_SESSION_TABLE);
00303        return (message_sent);
00304 }
00305 
00306 /*
00307  * send instant messages
00308  */
00309 void cmd_sexp(char *argbuf)
00310 {
00311        int message_sent = 0;
00312        char x_user[USERNAME_SIZE];
00313        char x_msg[1024];
00314        char *lun;
00315        char *lem;
00316        char *x_big_msgbuf = NULL;
00317 
00318        if ((!(CC->logged_in)) && (!(CC->internal_pgm))) {
00319               cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN);
00320               return;
00321        }
00322        if (CC->fake_username[0])
00323               lun = CC->fake_username;
00324        else
00325               lun = CC->user.fullname;
00326 
00327        lem = CC->cs_inet_email;
00328 
00329        extract_token(x_user, argbuf, 0, '|', sizeof x_user);
00330        extract_token(x_msg, argbuf, 1, '|', sizeof x_msg);
00331 
00332        if (!x_user[0]) {
00333               cprintf("%d You were not previously paged.\n", ERROR + NO_SUCH_USER);
00334               return;
00335        }
00336        if ((!strcasecmp(x_user, "broadcast")) && (CC->user.axlevel < AxAideU)) {
00337               cprintf("%d Higher access required to send a broadcast.\n",
00338                      ERROR + HIGHER_ACCESS_REQUIRED);
00339               return;
00340        }
00341        /* This loop handles text-transfer pages */
00342        if (!strcmp(x_msg, "-")) {
00343               message_sent = PerformXmsgHooks(lun, lem, x_user, "");
00344               if (message_sent == 0) {
00345                      if (CtdlGetUser(NULL, x_user))
00346                             cprintf("%d '%s' does not exist.\n",
00347                                           ERROR + NO_SUCH_USER, x_user);
00348                      else
00349                             cprintf("%d '%s' is not logged in "
00350                                           "or is not accepting pages.\n",
00351                                           ERROR + RESOURCE_NOT_OPEN, x_user);
00352                      return;
00353               }
00354               unbuffer_output();
00355               cprintf("%d Transmit message (will deliver to %d users)\n",
00356                      SEND_LISTING, message_sent);
00357               x_big_msgbuf = malloc(SIZ);
00358               memset(x_big_msgbuf, 0, SIZ);
00359               while (client_getln(x_msg, sizeof x_msg) >= 0 && strcmp(x_msg, "000")) {
00360                      x_big_msgbuf = realloc(x_big_msgbuf,
00361                             strlen(x_big_msgbuf) + strlen(x_msg) + 4);
00362                      if (!IsEmptyStr(x_big_msgbuf))
00363                         if (x_big_msgbuf[strlen(x_big_msgbuf)] != '\n')
00364                             strcat(x_big_msgbuf, "\n");
00365                      strcat(x_big_msgbuf, x_msg);
00366               }
00367               PerformXmsgHooks(lun, lem, x_user, x_big_msgbuf);
00368               free(x_big_msgbuf);
00369 
00370               /* This loop handles inline pages */
00371        } else {
00372               message_sent = PerformXmsgHooks(lun, lem, x_user, x_msg);
00373 
00374               if (message_sent > 0) {
00375                      if (!IsEmptyStr(x_msg))
00376                             cprintf("%d Message sent", CIT_OK);
00377                      else
00378                             cprintf("%d Ok to send message", CIT_OK);
00379                      if (message_sent > 1)
00380                             cprintf(" to %d users", message_sent);
00381                      cprintf(".\n");
00382               } else {
00383                      if (CtdlGetUser(NULL, x_user))
00384                             cprintf("%d '%s' does not exist.\n",
00385                                           ERROR + NO_SUCH_USER, x_user);
00386                      else
00387                             cprintf("%d '%s' is not logged in "
00388                                           "or is not accepting pages.\n",
00389                                           ERROR + RESOURCE_NOT_OPEN, x_user);
00390               }
00391 
00392 
00393        }
00394 }
00395 
00396 
00397 
00398 /*
00399  * Enter or exit paging-disabled mode
00400  */
00401 void cmd_dexp(char *argbuf)
00402 {
00403        int new_state;
00404 
00405        if (CtdlAccessCheck(ac_logged_in)) return;
00406 
00407        new_state = extract_int(argbuf, 0);
00408        if ((new_state == 0) || (new_state == 1)) {
00409               CC->disable_exp = new_state;
00410        }
00411 
00412        cprintf("%d %d\n", CIT_OK, CC->disable_exp);
00413 }
00414 
00415 
00416 /*
00417  * Request client termination
00418  */
00419 void cmd_reqt(char *argbuf) {
00420        struct CitContext *ccptr;
00421        int sessions = 0;
00422        int which_session;
00423        struct ExpressMessage *newmsg;
00424 
00425        if (CtdlAccessCheck(ac_aide)) return;
00426        which_session = extract_int(argbuf, 0);
00427 
00428        begin_critical_section(S_SESSION_TABLE);
00429        for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
00430               if ((ccptr->cs_pid == which_session) || (which_session == 0)) {
00431 
00432                      newmsg = (struct ExpressMessage *)
00433                             malloc(sizeof (struct ExpressMessage));
00434                      memset(newmsg, 0,
00435                             sizeof (struct ExpressMessage));
00436                      time(&(newmsg->timestamp));
00437                      safestrncpy(newmsg->sender, CC->user.fullname,
00438                                 sizeof newmsg->sender);
00439                      newmsg->flags |= EM_GO_AWAY;
00440                      newmsg->text = strdup("Automatic logoff requested.");
00441 
00442                      add_xmsg_to_context(ccptr, newmsg);
00443                      ++sessions;
00444 
00445               }
00446        }
00447        end_critical_section(S_SESSION_TABLE);
00448        cprintf("%d Sent termination request to %d sessions.\n", CIT_OK, sessions);
00449 }
00450 
00451 
00452 /*
00453  * This is the back end for flush_conversations_to_disk()
00454  * At this point we've isolated a single conversation (struct imlog)
00455  * and are ready to write it to disk.
00456  */
00457 void flush_individual_conversation(struct imlog *im) {
00458        struct CtdlMessage *msg;
00459        long msgnum = 0;
00460        char roomname[ROOMNAMELEN];
00461 
00462        StrBufAppendBufPlain(im->conversation, HKEY(
00463               "</body>\r\n"
00464               "</html>\r\n"
00465               ), 0
00466        );
00467 
00468        msg = malloc(sizeof(struct CtdlMessage));
00469        memset(msg, 0, sizeof(struct CtdlMessage));
00470        msg->cm_magic = CTDLMESSAGE_MAGIC;
00471        msg->cm_anon_type = MES_NORMAL;
00472        msg->cm_format_type = FMT_RFC822;
00473        if (!IsEmptyStr(im->usernames[0])) {
00474               msg->cm_fields['A'] = strdup(im->usernames[0]);
00475        } else {
00476               msg->cm_fields['A'] = strdup("Citadel");
00477        }
00478        if (!IsEmptyStr(im->usernames[1])) {
00479               msg->cm_fields['R'] = strdup(im->usernames[1]);
00480        }
00481        msg->cm_fields['O'] = strdup(PAGELOGROOM);
00482        msg->cm_fields['N'] = strdup(NODENAME);
00483        msg->cm_fields['M'] = SmashStrBuf(&im->conversation);   /* we own this memory now */
00484 
00485        /* Start with usernums[1] because it's guaranteed to be higher than usernums[0],
00486         * so if there's only one party, usernums[0] will be zero but usernums[1] won't.
00487         * Create the room if necessary.  Note that we create as a type 5 room rather
00488         * than 4, which indicates that it's a personal room but we've already supplied
00489         * the namespace prefix.
00490         *
00491         * In the unlikely event that usernums[1] is zero, a room with an invalid namespace
00492         * prefix will be created.  That's ok because the auto-purger will clean it up later.
00493         */
00494        snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[1], PAGELOGROOM);
00495        CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
00496        msgnum = CtdlSubmitMsg(msg, NULL, roomname, 0);
00497        CtdlFreeMessage(msg);
00498 
00499        /* If there is a valid user number in usernums[0], save a copy for them too. */
00500        if (im->usernums[0] > 0) {
00501               snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[0], PAGELOGROOM);
00502               CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
00503               CtdlSaveMsgPointerInRoom(roomname, msgnum, 0, NULL);
00504        }
00505 
00506        /* Finally, if we're logging instant messages globally, do that now. */
00507        if (!IsEmptyStr(config.c_logpages)) {
00508               CtdlCreateRoom(config.c_logpages, 3, "", 0, 1, 1, VIEW_BBS);
00509               CtdlSaveMsgPointerInRoom(config.c_logpages, msgnum, 0, NULL);
00510        }
00511 
00512 }
00513 
00514 /*
00515  * Locate instant message conversations which have gone idle
00516  * (or, if the server is shutting down, locate *all* conversations)
00517  * and flush them to disk (in the participants' log rooms, etc.)
00518  */
00519 void flush_conversations_to_disk(time_t if_older_than) {
00520 
00521        struct imlog *flush_these = NULL;
00522        struct imlog *dont_flush_these = NULL;
00523        struct imlog *imptr = NULL;
00524        struct CitContext *nptr;
00525        int nContexts, i;
00526 
00527        nptr = CtdlGetContextArray(&nContexts) ;  /* Make a copy of the current wholist */
00528 
00529        begin_critical_section(S_IM_LOGS);
00530        while (imlist)
00531        {
00532               imptr = imlist;
00533               imlist = imlist->next;
00534 
00535               /* For a two party conversation, if one party has logged out, force flush. */
00536               if (nptr) {
00537                      int user0_is_still_online = 0;
00538                      int user1_is_still_online = 0;
00539                      for (i=0; i<nContexts; i++)  {
00540                             if (nptr[i].user.usernum == imptr->usernums[0]) ++user0_is_still_online;
00541                             if (nptr[i].user.usernum == imptr->usernums[1]) ++user1_is_still_online;
00542                      }
00543                      if (imptr->usernums[0] != imptr->usernums[1]) {         /* two party conversation */
00544                             if ((!user0_is_still_online) || (!user1_is_still_online)) {
00545                                    imptr->lastmsg = 0L; /* force flush */
00546                             }
00547                      }
00548                      else {        /* one party conversation (yes, people do IM themselves) */
00549                             if (!user0_is_still_online) {
00550                                    imptr->lastmsg = 0L; /* force flush */
00551                             }
00552                      }
00553               }
00554 
00555               /* Now test this conversation to see if it qualifies for flushing. */
00556               if ((time(NULL) - imptr->lastmsg) > if_older_than)
00557               {
00558                      /* This conversation qualifies.  Move it to the list of ones to flush. */
00559                      imptr->next = flush_these;
00560                      flush_these = imptr;
00561               }
00562               else  {
00563                      /* Move it to the list of ones not to flush. */
00564                      imptr->next = dont_flush_these;
00565                      dont_flush_these = imptr;
00566               }
00567        }
00568        imlist = dont_flush_these;
00569        end_critical_section(S_IM_LOGS);
00570        free(nptr);
00571 
00572        /* We are now outside of the critical section, and we are the only thread holding a
00573         * pointer to a linked list of conversations to be flushed to disk.
00574         */
00575        while (flush_these) {
00576 
00577               flush_individual_conversation(flush_these);      /* This will free the string buffer */
00578               imptr = flush_these;
00579               flush_these = flush_these->next;
00580               free(imptr);
00581        }
00582 }
00583 
00584 
00585 
00586 void instmsg_timer(void) {
00587        flush_conversations_to_disk(300);  /* Anything that hasn't peeped in more than 5 minutes */
00588 }
00589 
00590 void instmsg_shutdown(void) {
00591        flush_conversations_to_disk(0);           /* Get it ALL onto disk NOW. */
00592 }
00593 
00594 CTDL_MODULE_INIT(instmsg)
00595 {
00596        if (!threading)
00597        {
00598               CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
00599               CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
00600               CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
00601               CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
00602               CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC, PRIO_ASYNC + 1);
00603               CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP, PRIO_STOP + 1);
00604               CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
00605               CtdlRegisterSessionHook(instmsg_timer, EVT_TIMER, PRIO_CLEANUP + 400);
00606               CtdlRegisterSessionHook(instmsg_shutdown, EVT_SHUTDOWN, PRIO_SHUTDOWN + 10);
00607        }
00608        
00609        /* return our module name for the log */
00610        return "instmsg";
00611 }