2 * This module handles instant messaging between users.
4 * Copyright (c) 1987-2020 by the citadel.org team
6 * This program is open source software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
23 #include <sys/types.h>
28 #include <libcitadel.h>
31 #include "serv_instmsg.h"
32 #include "citserver.h"
37 #include "ctdl_module.h"
42 char usernames[2][128];
48 struct imlog *imlist = NULL;
51 * This function handles the logging of instant messages to disk.
53 void log_instant_message(struct CitContext *me, struct CitContext *them, char *msgtext, int serial_number)
57 struct imlog *iptr = NULL;
58 struct imlog *this_im = NULL;
60 memset(usernums, 0, sizeof usernums);
61 usernums[0] = me->user.usernum;
62 usernums[1] = them->user.usernum;
64 /* Always put the lower user number first, so we can use the array as a hash value which
65 * represents a pair of users. For a broadcast message one of the users will be 0.
67 if (usernums[0] > usernums[1]) {
69 usernums[0] = usernums[1];
73 begin_critical_section(S_IM_LOGS);
75 /* Look for an existing conversation in the hash table.
76 * If not found, create a new one.
80 for (iptr = imlist; iptr != NULL; iptr = iptr->next) {
81 if ((iptr->usernums[0] == usernums[0]) && (iptr->usernums[1] == usernums[1])) {
82 /* Existing conversation */
86 if (this_im == NULL) {
87 /* New conversation */
88 this_im = malloc(sizeof(struct imlog));
89 memset(this_im, 0, sizeof (struct imlog));
90 this_im->usernums[0] = usernums[0];
91 this_im->usernums[1] = usernums[1];
92 /* usernames[] and usernums[] might not be in the same order. This is not an error. */
94 safestrncpy(this_im->usernames[0], me->user.fullname, sizeof this_im->usernames[0]);
97 safestrncpy(this_im->usernames[1], them->user.fullname, sizeof this_im->usernames[1]);
99 this_im->conversation = NewStrBuf();
100 this_im->next = imlist;
102 StrBufAppendBufPlain(this_im->conversation, HKEY("<html><body>\r\n"), 0);
105 /* Since it's possible for this function to get called more than once if a user is logged
106 * in on multiple sessions, we use the message's serial number to keep track of whether
107 * we've already logged it.
109 if (this_im->last_serial != serial_number)
111 this_im->lastmsg = time(NULL); /* Touch the timestamp so we know when to flush */
112 this_im->last_serial = serial_number;
113 StrBufAppendBufPlain(this_im->conversation, HKEY("<p><b>"), 0);
114 StrBufAppendBufPlain(this_im->conversation, me->user.fullname, -1, 0);
115 StrBufAppendBufPlain(this_im->conversation, HKEY(":</b> "), 0);
116 StrEscAppend(this_im->conversation, NULL, msgtext, 0, 0);
117 StrBufAppendBufPlain(this_im->conversation, HKEY("</p>\r\n"), 0);
119 end_critical_section(S_IM_LOGS);
124 * Delete any remaining instant messages
126 void delete_instant_messages(void) {
127 struct ExpressMessage *ptr;
129 begin_critical_section(S_SESSION_TABLE);
130 while (CC->FirstExpressMessage != NULL) {
131 ptr = CC->FirstExpressMessage->next;
132 if (CC->FirstExpressMessage->text != NULL)
133 free(CC->FirstExpressMessage->text);
134 free(CC->FirstExpressMessage);
135 CC->FirstExpressMessage = ptr;
137 end_critical_section(S_SESSION_TABLE);
142 * Retrieve instant messages
144 void cmd_gexp(char *argbuf) {
145 struct ExpressMessage *ptr;
147 if (CC->FirstExpressMessage == NULL) {
148 cprintf("%d No instant messages waiting.\n", ERROR + MESSAGE_NOT_FOUND);
152 begin_critical_section(S_SESSION_TABLE);
153 ptr = CC->FirstExpressMessage;
154 CC->FirstExpressMessage = CC->FirstExpressMessage->next;
155 end_critical_section(S_SESSION_TABLE);
157 cprintf("%d %d|%ld|%d|%s|%s|%s\n",
159 ((ptr->next != NULL) ? 1 : 0), /* more msgs? */
160 (long)ptr->timestamp, /* time sent */
161 ptr->flags, /* flags */
162 ptr->sender, /* sender of msg */
163 CtdlGetConfigStr("c_nodename"), /* static for now (and possibly deprecated) */
164 ptr->sender_email /* email or jid of sender */
167 if (ptr->text != NULL) {
168 memfmout(ptr->text, "\n");
178 * Asynchronously deliver instant messages
180 void cmd_gexp_async(void) {
182 /* Only do this if the session can handle asynchronous protocol */
183 if (CC->is_async == 0) return;
185 /* And don't do it if there's nothing to send. */
186 if (CC->FirstExpressMessage == NULL) return;
188 cprintf("%d instant msg\n", ASYNC_MSG + ASYNC_GEXP);
193 * Back end support function for send_instant_message() and company
195 void add_xmsg_to_context(struct CitContext *ccptr, struct ExpressMessage *newmsg)
197 struct ExpressMessage *findend;
199 if (ccptr->FirstExpressMessage == NULL) {
200 ccptr->FirstExpressMessage = newmsg;
203 findend = ccptr->FirstExpressMessage;
204 while (findend->next != NULL) {
205 findend = findend->next;
207 findend->next = newmsg;
210 /* If the target context is a session which can handle asynchronous
211 * messages, go ahead and set the flag for that.
213 set_async_waiting(ccptr);
218 * This is the back end to the instant message sending function.
219 * Returns the number of users to which the message was sent.
220 * Sending a zero-length message tests for recipients without sending messages.
222 int send_instant_message(char *lun, char *lem, char *x_user, char *x_msg)
224 int message_sent = 0; /* number of successful sends */
225 struct CitContext *ccptr;
226 struct ExpressMessage *newmsg = NULL;
227 int do_send = 0; /* 1 = send message; 0 = only check for valid recipient */
228 static int serial_number = 0; /* this keeps messages from getting logged twice */
230 if (!IsEmptyStr(x_msg)) {
234 /* find the target user's context and append the message */
235 begin_critical_section(S_SESSION_TABLE);
237 for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
239 if ( ((!strcasecmp(ccptr->user.fullname, x_user))
240 || (!strcasecmp(x_user, "broadcast")))
241 && (ccptr->can_receive_im)
242 && ((ccptr->disable_exp == 0)
243 || (CC->user.axlevel >= AxAideU)) ) {
245 newmsg = (struct ExpressMessage *) malloc(sizeof (struct ExpressMessage));
246 memset(newmsg, 0, sizeof (struct ExpressMessage));
247 time(&(newmsg->timestamp));
248 safestrncpy(newmsg->sender, lun, sizeof newmsg->sender);
249 safestrncpy(newmsg->sender_email, lem, sizeof newmsg->sender_email);
250 if (!strcasecmp(x_user, "broadcast")) {
251 newmsg->flags |= EM_BROADCAST;
253 newmsg->text = strdup(x_msg);
255 add_xmsg_to_context(ccptr, newmsg);
259 log_instant_message(CC, ccptr, newmsg->text, serial_number);
265 end_critical_section(S_SESSION_TABLE);
266 return (message_sent);
271 * send instant messages
273 void cmd_sexp(char *argbuf)
275 int message_sent = 0;
276 char x_user[USERNAME_SIZE];
279 char *x_big_msgbuf = NULL;
281 if ((!(CC->logged_in)) && (!(CC->internal_pgm))) {
282 cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN);
286 lem = CC->cs_principal_id;
288 extract_token(x_user, argbuf, 0, '|', sizeof x_user);
289 extract_token(x_msg, argbuf, 1, '|', sizeof x_msg);
292 cprintf("%d You were not previously paged.\n", ERROR + NO_SUCH_USER);
295 if ((!strcasecmp(x_user, "broadcast")) && (CC->user.axlevel < AxAideU)) {
296 cprintf("%d Higher access required to send a broadcast.\n",
297 ERROR + HIGHER_ACCESS_REQUIRED);
300 /* This loop handles text-transfer pages */
301 if (!strcmp(x_msg, "-")) {
302 message_sent = PerformXmsgHooks(CC->user.fullname, lem, x_user, "");
303 if (message_sent == 0) {
304 if (CtdlGetUser(NULL, x_user))
305 cprintf("%d '%s' does not exist.\n",
306 ERROR + NO_SUCH_USER, x_user);
308 cprintf("%d '%s' is not logged in "
309 "or is not accepting pages.\n",
310 ERROR + RESOURCE_NOT_OPEN, x_user);
314 cprintf("%d Transmit message (will deliver to %d users)\n",
315 SEND_LISTING, message_sent);
316 x_big_msgbuf = malloc(SIZ);
317 memset(x_big_msgbuf, 0, SIZ);
318 while (client_getln(x_msg, sizeof x_msg) >= 0 && strcmp(x_msg, "000")) {
319 x_big_msgbuf = realloc(x_big_msgbuf,
320 strlen(x_big_msgbuf) + strlen(x_msg) + 4);
321 if (!IsEmptyStr(x_big_msgbuf))
322 if (x_big_msgbuf[strlen(x_big_msgbuf)] != '\n')
323 strcat(x_big_msgbuf, "\n");
324 strcat(x_big_msgbuf, x_msg);
326 PerformXmsgHooks(CC->user.fullname, lem, x_user, x_big_msgbuf);
329 /* This loop handles inline pages */
331 message_sent = PerformXmsgHooks(CC->user.fullname, lem, x_user, x_msg);
333 if (message_sent > 0) {
334 if (!IsEmptyStr(x_msg)) {
335 cprintf("%d Message sent", CIT_OK);
338 cprintf("%d Ok to send message", CIT_OK);
340 if (message_sent > 1) {
341 cprintf(" to %d users", message_sent);
345 if (CtdlGetUser(NULL, x_user)) {
346 cprintf("%d '%s' does not exist.\n", ERROR + NO_SUCH_USER, x_user);
349 cprintf("%d '%s' is not logged in or is not accepting instant messages.\n",
350 ERROR + RESOURCE_NOT_OPEN, x_user);
360 * Enter or exit paging-disabled mode
362 void cmd_dexp(char *argbuf)
366 if (CtdlAccessCheck(ac_logged_in)) return;
368 new_state = extract_int(argbuf, 0);
369 if ((new_state == 0) || (new_state == 1)) {
370 CC->disable_exp = new_state;
373 cprintf("%d %d\n", CIT_OK, CC->disable_exp);
378 * Request client termination
380 void cmd_reqt(char *argbuf) {
381 struct CitContext *ccptr;
384 struct ExpressMessage *newmsg;
386 if (CtdlAccessCheck(ac_aide)) return;
387 which_session = extract_int(argbuf, 0);
389 begin_critical_section(S_SESSION_TABLE);
390 for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
391 if ((ccptr->cs_pid == which_session) || (which_session == 0)) {
393 newmsg = (struct ExpressMessage *)
394 malloc(sizeof (struct ExpressMessage));
396 sizeof (struct ExpressMessage));
397 time(&(newmsg->timestamp));
398 safestrncpy(newmsg->sender, CC->user.fullname,
399 sizeof newmsg->sender);
400 newmsg->flags |= EM_GO_AWAY;
401 newmsg->text = strdup("Automatic logoff requested.");
403 add_xmsg_to_context(ccptr, newmsg);
408 end_critical_section(S_SESSION_TABLE);
409 cprintf("%d Sent termination request to %d sessions.\n", CIT_OK, sessions);
414 * This is the back end for flush_conversations_to_disk()
415 * At this point we've isolated a single conversation (struct imlog)
416 * and are ready to write it to disk.
418 void flush_individual_conversation(struct imlog *im) {
419 struct CtdlMessage *msg;
421 char roomname[ROOMNAMELEN];
422 StrBuf *MsgBuf, *FullMsgBuf;
424 StrBufAppendBufPlain(im->conversation, HKEY(
430 MsgBuf = StrBufRFC2047encodeMessage(im->conversation);
431 FlushStrBuf(im->conversation);
432 FullMsgBuf = NewStrBufPlain(NULL, StrLength(im->conversation) + 100);
434 StrBufAppendBufPlain(FullMsgBuf, HKEY(
435 "Content-type: text/html; charset=UTF-8\r\n"
436 "Content-Transfer-Encoding: quoted-printable\r\n"
440 StrBufAppendBuf (FullMsgBuf, MsgBuf, 0);
443 msg = malloc(sizeof(struct CtdlMessage));
444 memset(msg, 0, sizeof(struct CtdlMessage));
445 msg->cm_magic = CTDLMESSAGE_MAGIC;
446 msg->cm_anon_type = MES_NORMAL;
447 msg->cm_format_type = FMT_RFC822;
448 if (!IsEmptyStr(im->usernames[0])) {
449 CM_SetField(msg, eAuthor, im->usernames[0], strlen(im->usernames[0]));
451 CM_SetField(msg, eAuthor, HKEY("Citadel"));
453 if (!IsEmptyStr(im->usernames[1])) {
454 CM_SetField(msg, eRecipient, im->usernames[1], strlen(im->usernames[1]));
457 CM_SetField(msg, eOriginalRoom, HKEY(PAGELOGROOM));
458 CM_SetAsFieldSB(msg, eMesageText, &FullMsgBuf); /* we own this memory now */
460 /* Start with usernums[1] because it's guaranteed to be higher than usernums[0],
461 * so if there's only one party, usernums[0] will be zero but usernums[1] won't.
462 * Create the room if necessary. Note that we create as a type 5 room rather
463 * than 4, which indicates that it's a personal room but we've already supplied
464 * the namespace prefix.
466 * In the unlikely event that usernums[1] is zero, a room with an invalid namespace
467 * prefix will be created. That's ok because the auto-purger will clean it up later.
469 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[1], PAGELOGROOM);
470 CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
471 msgnum = CtdlSubmitMsg(msg, NULL, roomname);
474 /* If there is a valid user number in usernums[0], save a copy for them too. */
475 if (im->usernums[0] > 0) {
476 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[0], PAGELOGROOM);
477 CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
478 CtdlSaveMsgPointerInRoom(roomname, msgnum, 0, NULL);
481 /* Finally, if we're logging instant messages globally, do that now. */
482 if (!IsEmptyStr(CtdlGetConfigStr("c_logpages"))) {
483 CtdlCreateRoom(CtdlGetConfigStr("c_logpages"), 3, "", 0, 1, 1, VIEW_BBS);
484 CtdlSaveMsgPointerInRoom(CtdlGetConfigStr("c_logpages"), msgnum, 0, NULL);
490 * Locate instant message conversations which have gone idle
491 * (or, if the server is shutting down, locate *all* conversations)
492 * and flush them to disk (in the participants' log rooms, etc.)
494 void flush_conversations_to_disk(time_t if_older_than) {
496 struct imlog *flush_these = NULL;
497 struct imlog *dont_flush_these = NULL;
498 struct imlog *imptr = NULL;
499 struct CitContext *nptr;
502 nptr = CtdlGetContextArray(&nContexts) ; /* Make a copy of the current wholist */
504 begin_critical_section(S_IM_LOGS);
508 imlist = imlist->next;
510 /* For a two party conversation, if one party has logged out, force flush. */
512 int user0_is_still_online = 0;
513 int user1_is_still_online = 0;
514 for (i=0; i<nContexts; i++) {
515 if (nptr[i].user.usernum == imptr->usernums[0]) ++user0_is_still_online;
516 if (nptr[i].user.usernum == imptr->usernums[1]) ++user1_is_still_online;
518 if (imptr->usernums[0] != imptr->usernums[1]) { /* two party conversation */
519 if ((!user0_is_still_online) || (!user1_is_still_online)) {
520 imptr->lastmsg = 0L; /* force flush */
523 else { /* one party conversation (yes, people do IM themselves) */
524 if (!user0_is_still_online) {
525 imptr->lastmsg = 0L; /* force flush */
530 /* Now test this conversation to see if it qualifies for flushing. */
531 if ((time(NULL) - imptr->lastmsg) > if_older_than)
533 /* This conversation qualifies. Move it to the list of ones to flush. */
534 imptr->next = flush_these;
538 /* Move it to the list of ones not to flush. */
539 imptr->next = dont_flush_these;
540 dont_flush_these = imptr;
543 imlist = dont_flush_these;
544 end_critical_section(S_IM_LOGS);
547 /* We are now outside of the critical section, and we are the only thread holding a
548 * pointer to a linked list of conversations to be flushed to disk.
550 while (flush_these) {
552 flush_individual_conversation(flush_these); /* This will free the string buffer */
554 flush_these = flush_these->next;
561 void instmsg_timer(void) {
562 flush_conversations_to_disk(300); /* Anything that hasn't peeped in more than 5 minutes */
565 void instmsg_shutdown(void) {
566 flush_conversations_to_disk(0); /* Get it ALL onto disk NOW. */
569 CTDL_MODULE_INIT(instmsg)
573 CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
574 CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
575 CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
576 CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
577 CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC, PRIO_ASYNC + 1);
578 CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP, PRIO_STOP + 1);
579 CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
580 CtdlRegisterSessionHook(instmsg_timer, EVT_TIMER, PRIO_CLEANUP + 400);
581 CtdlRegisterSessionHook(instmsg_shutdown, EVT_SHUTDOWN, PRIO_SHUTDOWN + 10);
584 /* return our module name for the log */