4 * This module handles instant messaging between users.
6 * Copyright (c) 1987-2010 by the citadel.org team
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 3 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
31 #include <sys/types.h>
33 #if TIME_WITH_SYS_TIME
34 # include <sys/time.h>
38 # include <sys/time.h>
47 #include <libcitadel.h>
50 #include "serv_instmsg.h"
51 #include "citserver.h"
61 #include "ctdl_module.h"
66 char usernames[2][128];
72 struct imlog *imlist = NULL;
75 * This function handles the logging of instant messages to disk.
77 void log_instant_message(struct CitContext *me, struct CitContext *them, char *msgtext, int serial_number)
81 struct imlog *iptr = NULL;
82 struct imlog *this_im = NULL;
84 memset(usernums, 0, sizeof usernums);
85 usernums[0] = me->user.usernum;
86 usernums[1] = them->user.usernum;
88 /* Always put the lower user number first, so we can use the array as a hash value which
89 * represents a pair of users. For a broadcast message one of the users will be 0.
91 if (usernums[0] > usernums[1]) {
93 usernums[0] = usernums[1];
97 begin_critical_section(S_IM_LOGS);
99 /* Look for an existing conversation in the hash table.
100 * If not found, create a new one.
104 for (iptr = imlist; iptr != NULL; iptr = iptr->next) {
105 if ((iptr->usernums[0] == usernums[0]) && (iptr->usernums[1] == usernums[1])) {
106 /* Existing conversation */
110 if (this_im == NULL) {
111 /* New conversation */
112 this_im = malloc(sizeof(struct imlog));
113 memset(this_im, 0, sizeof (struct imlog));
114 this_im->usernums[0] = usernums[0];
115 this_im->usernums[1] = usernums[1];
116 /* usernames[] and usernums[] might not be in the same order. This is not an error. */
118 safestrncpy(this_im->usernames[0], me->user.fullname, sizeof this_im->usernames[0]);
121 safestrncpy(this_im->usernames[1], them->user.fullname, sizeof this_im->usernames[1]);
123 this_im->conversation = NewStrBuf();
124 this_im->next = imlist;
126 StrBufAppendBufPlain(this_im->conversation, HKEY(
127 "Content-type: text/html\r\n"
128 "Content-transfer-encoding: 7bit\r\n"
135 /* Since it's possible for this function to get called more than once if a user is logged
136 * in on multiple sessions, we use the message's serial number to keep track of whether
137 * we've already logged it.
139 if (this_im->last_serial != serial_number)
141 this_im->lastmsg = time(NULL); /* Touch the timestamp so we know when to flush */
142 this_im->last_serial = serial_number;
143 StrBufAppendBufPlain(this_im->conversation, HKEY("<p><b>"), 0);
144 StrBufAppendBufPlain(this_im->conversation, me->user.fullname, -1, 0);
145 StrBufAppendBufPlain(this_im->conversation, HKEY(":</b> "), 0);
146 StrEscAppend(this_im->conversation, NULL, msgtext, 0, 0);
147 StrBufAppendBufPlain(this_im->conversation, HKEY("</p>\r\n"), 0);
149 end_critical_section(S_IM_LOGS);
154 * Delete any remaining instant messages
156 void delete_instant_messages(void) {
157 struct ExpressMessage *ptr;
159 begin_critical_section(S_SESSION_TABLE);
160 while (CC->FirstExpressMessage != NULL) {
161 ptr = CC->FirstExpressMessage->next;
162 if (CC->FirstExpressMessage->text != NULL)
163 free(CC->FirstExpressMessage->text);
164 free(CC->FirstExpressMessage);
165 CC->FirstExpressMessage = ptr;
167 end_critical_section(S_SESSION_TABLE);
173 * Retrieve instant messages
175 void cmd_gexp(char *argbuf) {
176 struct ExpressMessage *ptr;
178 if (CC->FirstExpressMessage == NULL) {
179 cprintf("%d No instant messages waiting.\n", ERROR + MESSAGE_NOT_FOUND);
183 begin_critical_section(S_SESSION_TABLE);
184 ptr = CC->FirstExpressMessage;
185 CC->FirstExpressMessage = CC->FirstExpressMessage->next;
186 end_critical_section(S_SESSION_TABLE);
188 cprintf("%d %d|%ld|%d|%s|%s|%s\n",
190 ((ptr->next != NULL) ? 1 : 0), /* more msgs? */
191 (long)ptr->timestamp, /* time sent */
192 ptr->flags, /* flags */
193 ptr->sender, /* sender of msg */
194 config.c_nodename, /* static for now (and possibly deprecated) */
195 ptr->sender_email /* email or jid of sender */
198 if (ptr->text != NULL) {
199 memfmout(ptr->text, "\n");
208 * Asynchronously deliver instant messages
210 void cmd_gexp_async(void) {
212 /* Only do this if the session can handle asynchronous protocol */
213 if (CC->is_async == 0) return;
215 /* And don't do it if there's nothing to send. */
216 if (CC->FirstExpressMessage == NULL) return;
218 cprintf("%d instant msg\n", ASYNC_MSG + ASYNC_GEXP);
222 * Back end support function for send_instant_message() and company
224 void add_xmsg_to_context(struct CitContext *ccptr, struct ExpressMessage *newmsg)
226 struct ExpressMessage *findend;
228 if (ccptr->FirstExpressMessage == NULL) {
229 ccptr->FirstExpressMessage = newmsg;
232 findend = ccptr->FirstExpressMessage;
233 while (findend->next != NULL) {
234 findend = findend->next;
236 findend->next = newmsg;
239 /* If the target context is a session which can handle asynchronous
240 * messages, go ahead and set the flag for that.
242 set_async_waiting(ccptr);
249 * This is the back end to the instant message sending function.
250 * Returns the number of users to which the message was sent.
251 * Sending a zero-length message tests for recipients without sending messages.
253 int send_instant_message(char *lun, char *lem, char *x_user, char *x_msg)
255 int message_sent = 0; /* number of successful sends */
256 struct CitContext *ccptr;
257 struct ExpressMessage *newmsg = NULL;
259 int do_send = 0; /* 1 = send message; 0 = only check for valid recipient */
260 static int serial_number = 0; /* this keeps messages from getting logged twice */
262 if (strlen(x_msg) > 0) {
266 /* find the target user's context and append the message */
267 begin_critical_section(S_SESSION_TABLE);
269 for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
271 if (ccptr->fake_username[0]) {
272 un = ccptr->fake_username;
275 un = ccptr->user.fullname;
278 if ( ((!strcasecmp(un, x_user))
279 || (!strcasecmp(x_user, "broadcast")))
280 && (ccptr->can_receive_im)
281 && ((ccptr->disable_exp == 0)
282 || (CC->user.axlevel >= AxAideU)) ) {
284 newmsg = (struct ExpressMessage *) malloc(sizeof (struct ExpressMessage));
285 memset(newmsg, 0, sizeof (struct ExpressMessage));
286 time(&(newmsg->timestamp));
287 safestrncpy(newmsg->sender, lun, sizeof newmsg->sender);
288 safestrncpy(newmsg->sender_email, lem, sizeof newmsg->sender_email);
289 if (!strcasecmp(x_user, "broadcast")) {
290 newmsg->flags |= EM_BROADCAST;
292 newmsg->text = strdup(x_msg);
294 add_xmsg_to_context(ccptr, newmsg);
298 log_instant_message(CC, ccptr, newmsg->text, serial_number);
304 end_critical_section(S_SESSION_TABLE);
305 return (message_sent);
309 * send instant messages
311 void cmd_sexp(char *argbuf)
313 int message_sent = 0;
314 char x_user[USERNAME_SIZE];
318 char *x_big_msgbuf = NULL;
320 if ((!(CC->logged_in)) && (!(CC->internal_pgm))) {
321 cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN);
324 if (CC->fake_username[0])
325 lun = CC->fake_username;
327 lun = CC->user.fullname;
329 lem = CC->cs_inet_email;
331 extract_token(x_user, argbuf, 0, '|', sizeof x_user);
332 extract_token(x_msg, argbuf, 1, '|', sizeof x_msg);
335 cprintf("%d You were not previously paged.\n", ERROR + NO_SUCH_USER);
338 if ((!strcasecmp(x_user, "broadcast")) && (CC->user.axlevel < AxAideU)) {
339 cprintf("%d Higher access required to send a broadcast.\n",
340 ERROR + HIGHER_ACCESS_REQUIRED);
343 /* This loop handles text-transfer pages */
344 if (!strcmp(x_msg, "-")) {
345 message_sent = PerformXmsgHooks(lun, lem, x_user, "");
346 if (message_sent == 0) {
347 if (CtdlGetUser(NULL, x_user))
348 cprintf("%d '%s' does not exist.\n",
349 ERROR + NO_SUCH_USER, x_user);
351 cprintf("%d '%s' is not logged in "
352 "or is not accepting pages.\n",
353 ERROR + RESOURCE_NOT_OPEN, x_user);
357 cprintf("%d Transmit message (will deliver to %d users)\n",
358 SEND_LISTING, message_sent);
359 x_big_msgbuf = malloc(SIZ);
360 memset(x_big_msgbuf, 0, SIZ);
361 while (client_getln(x_msg, sizeof x_msg) >= 0 && strcmp(x_msg, "000")) {
362 x_big_msgbuf = realloc(x_big_msgbuf,
363 strlen(x_big_msgbuf) + strlen(x_msg) + 4);
364 if (!IsEmptyStr(x_big_msgbuf))
365 if (x_big_msgbuf[strlen(x_big_msgbuf)] != '\n')
366 strcat(x_big_msgbuf, "\n");
367 strcat(x_big_msgbuf, x_msg);
369 PerformXmsgHooks(lun, lem, x_user, x_big_msgbuf);
372 /* This loop handles inline pages */
374 message_sent = PerformXmsgHooks(lun, lem, x_user, x_msg);
376 if (message_sent > 0) {
377 if (!IsEmptyStr(x_msg))
378 cprintf("%d Message sent", CIT_OK);
380 cprintf("%d Ok to send message", CIT_OK);
381 if (message_sent > 1)
382 cprintf(" to %d users", message_sent);
385 if (CtdlGetUser(NULL, x_user))
386 cprintf("%d '%s' does not exist.\n",
387 ERROR + NO_SUCH_USER, x_user);
389 cprintf("%d '%s' is not logged in "
390 "or is not accepting pages.\n",
391 ERROR + RESOURCE_NOT_OPEN, x_user);
401 * Enter or exit paging-disabled mode
403 void cmd_dexp(char *argbuf)
407 if (CtdlAccessCheck(ac_logged_in)) return;
409 new_state = extract_int(argbuf, 0);
410 if ((new_state == 0) || (new_state == 1)) {
411 CC->disable_exp = new_state;
414 cprintf("%d %d\n", CIT_OK, CC->disable_exp);
419 * Request client termination
421 void cmd_reqt(char *argbuf) {
422 struct CitContext *ccptr;
425 struct ExpressMessage *newmsg;
427 if (CtdlAccessCheck(ac_aide)) return;
428 which_session = extract_int(argbuf, 0);
430 begin_critical_section(S_SESSION_TABLE);
431 for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
432 if ((ccptr->cs_pid == which_session) || (which_session == 0)) {
434 newmsg = (struct ExpressMessage *)
435 malloc(sizeof (struct ExpressMessage));
437 sizeof (struct ExpressMessage));
438 time(&(newmsg->timestamp));
439 safestrncpy(newmsg->sender, CC->user.fullname,
440 sizeof newmsg->sender);
441 newmsg->flags |= EM_GO_AWAY;
442 newmsg->text = strdup("Automatic logoff requested.");
444 add_xmsg_to_context(ccptr, newmsg);
449 end_critical_section(S_SESSION_TABLE);
450 cprintf("%d Sent termination request to %d sessions.\n", CIT_OK, sessions);
455 * This is the back end for flush_conversations_to_disk()
456 * At this point we've isolated a single conversation (struct imlog)
457 * and are ready to write it to disk.
459 void flush_individual_conversation(struct imlog *im) {
460 struct CtdlMessage *msg;
462 char roomname[ROOMNAMELEN];
464 StrBufAppendBufPlain(im->conversation, HKEY(
470 msg = malloc(sizeof(struct CtdlMessage));
471 memset(msg, 0, sizeof(struct CtdlMessage));
472 msg->cm_magic = CTDLMESSAGE_MAGIC;
473 msg->cm_anon_type = MES_NORMAL;
474 msg->cm_format_type = FMT_RFC822;
475 if (!IsEmptyStr(im->usernames[0])) {
476 msg->cm_fields['A'] = strdup(im->usernames[0]);
478 msg->cm_fields['A'] = strdup("Citadel");
480 if (!IsEmptyStr(im->usernames[1])) {
481 msg->cm_fields['R'] = strdup(im->usernames[1]);
483 msg->cm_fields['O'] = strdup(PAGELOGROOM);
484 msg->cm_fields['N'] = strdup(NODENAME);
485 msg->cm_fields['M'] = SmashStrBuf(&im->conversation); /* we own this memory now */
487 /* Start with usernums[1] because it's guaranteed to be higher than usernums[0],
488 * so if there's only one party, usernums[0] will be zero but usernums[1] won't.
489 * Create the room if necessary. Note that we create as a type 5 room rather
490 * than 4, which indicates that it's a personal room but we've already supplied
491 * the namespace prefix.
493 * In the unlikely event that usernums[1] is zero, a room with an invalid namespace
494 * prefix will be created. That's ok because the auto-purger will clean it up later.
496 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[1], PAGELOGROOM);
497 CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
498 msgnum = CtdlSubmitMsg(msg, NULL, roomname, 0);
499 CtdlFreeMessage(msg);
501 /* If there is a valid user number in usernums[0], save a copy for them too. */
502 if (im->usernums[0] > 0) {
503 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[0], PAGELOGROOM);
504 CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
505 CtdlSaveMsgPointerInRoom(roomname, msgnum, 0, NULL);
508 /* Finally, if we're logging instant messages globally, do that now. */
509 if (!IsEmptyStr(config.c_logpages)) {
510 CtdlCreateRoom(config.c_logpages, 3, "", 0, 1, 1, VIEW_BBS);
511 CtdlSaveMsgPointerInRoom(config.c_logpages, msgnum, 0, NULL);
517 * Locate instant message conversations which have gone idle
518 * (or, if the server is shutting down, locate *all* conversations)
519 * and flush them to disk (in the participants' log rooms, etc.)
521 void flush_conversations_to_disk(time_t if_older_than) {
523 struct imlog *flush_these = NULL;
524 struct imlog *dont_flush_these = NULL;
525 struct imlog *imptr = NULL;
526 struct CitContext *nptr;
529 nptr = CtdlGetContextArray(&nContexts) ; /* Make a copy of the current wholist */
531 begin_critical_section(S_IM_LOGS);
535 imlist = imlist->next;
537 /* For a two party conversation, if one party has logged out, force flush. */
539 int user0_is_still_online = 0;
540 int user1_is_still_online = 0;
541 for (i=0; i<nContexts; i++) {
542 if (nptr[i].user.usernum == imptr->usernums[0]) ++user0_is_still_online;
543 if (nptr[i].user.usernum == imptr->usernums[1]) ++user1_is_still_online;
545 if (imptr->usernums[0] != imptr->usernums[1]) { /* two party conversation */
546 if ((!user0_is_still_online) || (!user1_is_still_online)) {
547 imptr->lastmsg = 0L; /* force flush */
550 else { /* one party conversation (yes, people do IM themselves) */
551 if (!user0_is_still_online) {
552 imptr->lastmsg = 0L; /* force flush */
557 /* Now test this conversation to see if it qualifies for flushing. */
558 if ((time(NULL) - imptr->lastmsg) > if_older_than)
560 /* This conversation qualifies. Move it to the list of ones to flush. */
561 imptr->next = flush_these;
565 /* Move it to the list of ones not to flush. */
566 imptr->next = dont_flush_these;
567 dont_flush_these = imptr;
570 imlist = dont_flush_these;
571 end_critical_section(S_IM_LOGS);
574 /* We are now outside of the critical section, and we are the only thread holding a
575 * pointer to a linked list of conversations to be flushed to disk.
577 while (flush_these) {
579 flush_individual_conversation(flush_these); /* This will free the string buffer */
581 flush_these = flush_these->next;
588 void instmsg_timer(void) {
589 flush_conversations_to_disk(300); /* Anything that hasn't peeped in more than 5 minutes */
592 void instmsg_shutdown(void) {
593 flush_conversations_to_disk(0); /* Get it ALL onto disk NOW. */
596 CTDL_MODULE_INIT(instmsg)
600 CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
601 CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
602 CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
603 CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
604 CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC);
605 CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP);
606 CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
607 CtdlRegisterSessionHook(instmsg_timer, EVT_TIMER);
608 CtdlRegisterSessionHook(instmsg_shutdown, EVT_SHUTDOWN);
611 /* return our Subversion id for the Log */