1 // This module handles instant messaging between users.
3 // Copyright (c) 1987-2023 by the citadel.org team
5 // This program is open source software. Use, duplication, or disclosure
6 // is subject to the terms of the GNU General Public License, version 3.
15 #include <sys/types.h>
20 #include <libcitadel.h>
21 #include "../../sysdep.h"
22 #include "../../citadel_defs.h"
23 #include "../../server.h"
24 #include "../../context.h"
25 #include "../../citserver.h"
26 #include "../../support.h"
27 #include "../../config.h"
28 #include "../../msgbase.h"
29 #include "../../user_ops.h"
30 #include "../../ctdl_module.h"
31 #include "serv_instmsg.h"
36 char usernames[2][128];
42 struct imlog *imlist = NULL;
44 // This function handles the logging of instant messages to disk.
45 void log_instant_message(struct CitContext *me, struct CitContext *them, char *msgtext, int serial_number) {
48 struct imlog *iptr = NULL;
49 struct imlog *this_im = NULL;
51 memset(usernums, 0, sizeof usernums);
52 usernums[0] = me->user.usernum;
53 usernums[1] = them->user.usernum;
55 // Always put the lower user number first, so we can use the array as a hash value which
56 // represents a pair of users. For a broadcast message one of the users will be 0.
57 if (usernums[0] > usernums[1]) {
59 usernums[0] = usernums[1];
63 begin_critical_section(S_IM_LOGS);
65 // Look for an existing conversation in the hash table.
66 // If not found, create a new one.
69 for (iptr = imlist; iptr != NULL; iptr = iptr->next) {
70 if ((iptr->usernums[0] == usernums[0]) && (iptr->usernums[1] == usernums[1])) {
71 /* Existing conversation */
75 if (this_im == NULL) {
77 this_im = malloc(sizeof(struct imlog));
78 memset(this_im, 0, sizeof (struct imlog));
79 this_im->usernums[0] = usernums[0];
80 this_im->usernums[1] = usernums[1];
81 // usernames[] and usernums[] might not be in the same order. This is not an error.
83 safestrncpy(this_im->usernames[0], me->user.fullname, sizeof this_im->usernames[0]);
86 safestrncpy(this_im->usernames[1], them->user.fullname, sizeof this_im->usernames[1]);
88 this_im->conversation = NewStrBuf();
89 this_im->next = imlist;
91 StrBufAppendBufPlain(this_im->conversation, HKEY("<html><body>\r\n"), 0);
94 // Since it's possible for this function to get called more than once if a user is logged
95 // in on multiple sessions, we use the message's serial number to keep track of whether
96 // we've already logged it.
97 if (this_im->last_serial != serial_number) {
98 this_im->lastmsg = time(NULL); // Touch the timestamp so we know when to flush
99 this_im->last_serial = serial_number;
100 StrBufAppendBufPlain(this_im->conversation, HKEY("<p><b>"), 0);
101 StrBufAppendBufPlain(this_im->conversation, me->user.fullname, -1, 0);
102 StrBufAppendBufPlain(this_im->conversation, HKEY(":</b> "), 0);
103 StrEscAppend(this_im->conversation, NULL, msgtext, 0, 0);
104 StrBufAppendBufPlain(this_im->conversation, HKEY("</p>\r\n"), 0);
106 end_critical_section(S_IM_LOGS);
110 // Delete any remaining instant messages
111 void delete_instant_messages(void) {
112 struct ExpressMessage *ptr;
114 begin_critical_section(S_SESSION_TABLE);
115 while (CC->FirstExpressMessage != NULL) {
116 ptr = CC->FirstExpressMessage->next;
117 if (CC->FirstExpressMessage->text != NULL)
118 free(CC->FirstExpressMessage->text);
119 free(CC->FirstExpressMessage);
120 CC->FirstExpressMessage = ptr;
122 end_critical_section(S_SESSION_TABLE);
126 // Retrieve instant messages
127 void cmd_gexp(char *argbuf) {
128 struct ExpressMessage *ptr;
130 if (CC->FirstExpressMessage == NULL) {
131 cprintf("%d No instant messages waiting.\n", ERROR + MESSAGE_NOT_FOUND);
135 begin_critical_section(S_SESSION_TABLE);
136 ptr = CC->FirstExpressMessage;
137 CC->FirstExpressMessage = CC->FirstExpressMessage->next;
138 end_critical_section(S_SESSION_TABLE);
140 cprintf("%d %d|%ld|%d|%s|%s|%s\n",
142 ((ptr->next != NULL) ? 1 : 0), // more msgs?
143 (long)ptr->timestamp, // time sent
145 ptr->sender, // sender of msg
146 CtdlGetConfigStr("c_nodename"), // static for now (and possibly deprecated)
147 ptr->sender_email // email or jid of sender
150 if (ptr->text != NULL) {
151 memfmout(ptr->text, "\n");
160 // Asynchronously deliver instant messages
161 void cmd_gexp_async(void) {
163 // Only do this if the session can handle asynchronous protocol
164 if (CC->is_async == 0) return;
166 // And don't do it if there's nothing to send.
167 if (CC->FirstExpressMessage == NULL) return;
169 cprintf("%d instant msg\n", ASYNC_MSG + ASYNC_GEXP);
173 // Back end support function for send_instant_message() and company
174 void add_xmsg_to_context(struct CitContext *ccptr, struct ExpressMessage *newmsg) {
175 struct ExpressMessage *findend;
177 if (ccptr->FirstExpressMessage == NULL) {
178 ccptr->FirstExpressMessage = newmsg;
181 findend = ccptr->FirstExpressMessage;
182 while (findend->next != NULL) {
183 findend = findend->next;
185 findend->next = newmsg;
188 // If the target context is a session which can handle asynchronous
189 // messages, go ahead and set the flag for that.
190 set_async_waiting(ccptr);
194 // This is the back end to the instant message sending function.
195 // Returns the number of users to which the message was sent.
196 // Sending a zero-length message tests for recipients without sending messages.
197 int send_instant_message(char *lun, char *lem, char *x_user, char *x_msg) {
198 int message_sent = 0; // number of successful sends
199 struct CitContext *ccptr;
200 struct ExpressMessage *newmsg = NULL;
201 int do_send = 0; // 1 = send message; 0 = only check for valid recipient
202 static int serial_number = 0; // this keeps messages from getting logged twice
204 if (!IsEmptyStr(x_msg)) {
208 // find the target user's context and append the message
209 begin_critical_section(S_SESSION_TABLE);
211 for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
213 if ( ((!strcasecmp(ccptr->user.fullname, x_user))
214 || (!strcasecmp(x_user, "broadcast")))
215 && (ccptr->can_receive_im)
216 && ((ccptr->disable_exp == 0)
217 || (CC->user.axlevel >= AxAideU)) ) {
219 newmsg = (struct ExpressMessage *) malloc(sizeof (struct ExpressMessage));
220 memset(newmsg, 0, sizeof (struct ExpressMessage));
221 time(&(newmsg->timestamp));
222 safestrncpy(newmsg->sender, lun, sizeof newmsg->sender);
223 safestrncpy(newmsg->sender_email, lem, sizeof newmsg->sender_email);
224 if (!strcasecmp(x_user, "broadcast")) {
225 newmsg->flags |= EM_BROADCAST;
227 newmsg->text = strdup(x_msg);
229 add_xmsg_to_context(ccptr, newmsg);
233 log_instant_message(CC, ccptr, newmsg->text, serial_number);
239 end_critical_section(S_SESSION_TABLE);
240 return (message_sent);
244 // send instant messages
245 void cmd_sexp(char *argbuf) {
246 int message_sent = 0;
247 char x_user[USERNAME_SIZE];
250 char *x_big_msgbuf = NULL;
252 if ((!(CC->logged_in)) && (!(CC->internal_pgm))) {
253 cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN);
257 lem = CC->cs_principal_id;
259 extract_token(x_user, argbuf, 0, '|', sizeof x_user);
260 extract_token(x_msg, argbuf, 1, '|', sizeof x_msg);
263 cprintf("%d You were not previously paged.\n", ERROR + NO_SUCH_USER);
266 if ((!strcasecmp(x_user, "broadcast")) && (CC->user.axlevel < AxAideU)) {
267 cprintf("%d Higher access required to send a broadcast.\n",
268 ERROR + HIGHER_ACCESS_REQUIRED);
271 // This loop handles text-transfer pages
272 if (!strcmp(x_msg, "-")) {
273 message_sent = PerformXmsgHooks(CC->user.fullname, lem, x_user, "");
274 if (message_sent == 0) {
275 if (CtdlGetUser(NULL, x_user))
276 cprintf("%d '%s' does not exist.\n", ERROR + NO_SUCH_USER, x_user);
278 cprintf("%d '%s' is not logged in or is not accepting messages.\n", ERROR + RESOURCE_NOT_OPEN, x_user);
282 cprintf("%d Transmit message (will deliver to %d users)\n", SEND_LISTING, message_sent);
283 x_big_msgbuf = malloc(SIZ);
284 memset(x_big_msgbuf, 0, SIZ);
285 while (client_getln(x_msg, sizeof x_msg) >= 0 && strcmp(x_msg, "000")) {
286 x_big_msgbuf = realloc(x_big_msgbuf, strlen(x_big_msgbuf) + strlen(x_msg) + 4);
287 if (!IsEmptyStr(x_big_msgbuf))
288 if (x_big_msgbuf[strlen(x_big_msgbuf)] != '\n')
289 strcat(x_big_msgbuf, "\n");
290 strcat(x_big_msgbuf, x_msg);
292 PerformXmsgHooks(CC->user.fullname, lem, x_user, x_big_msgbuf);
295 // This loop handles inline pages
298 message_sent = PerformXmsgHooks(CC->user.fullname, lem, x_user, x_msg);
300 if (message_sent > 0) {
301 if (!IsEmptyStr(x_msg)) {
302 cprintf("%d Message sent", CIT_OK);
305 cprintf("%d Ok to send message", CIT_OK);
307 if (message_sent > 1) {
308 cprintf(" to %d users", message_sent);
313 if (CtdlGetUser(NULL, x_user)) {
314 cprintf("%d '%s' does not exist.\n", ERROR + NO_SUCH_USER, x_user);
317 cprintf("%d '%s' is not logged in or is not accepting messages.\n", ERROR + RESOURCE_NOT_OPEN, x_user);
324 // Enter or exit paging-disabled mode
325 void cmd_dexp(char *argbuf) {
328 if (CtdlAccessCheck(ac_logged_in)) return;
330 new_state = extract_int(argbuf, 0);
331 if ((new_state == 0) || (new_state == 1)) {
332 CC->disable_exp = new_state;
335 cprintf("%d %d\n", CIT_OK, CC->disable_exp);
339 // Request client termination
340 void cmd_reqt(char *argbuf) {
341 struct CitContext *ccptr;
344 struct ExpressMessage *newmsg;
346 if (CtdlAccessCheck(ac_aide)) return;
347 which_session = extract_int(argbuf, 0);
349 begin_critical_section(S_SESSION_TABLE);
350 for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
351 if ((ccptr->cs_pid == which_session) || (which_session == 0)) {
353 newmsg = (struct ExpressMessage *)
354 malloc(sizeof (struct ExpressMessage));
356 sizeof (struct ExpressMessage));
357 time(&(newmsg->timestamp));
358 safestrncpy(newmsg->sender, CC->user.fullname,
359 sizeof newmsg->sender);
360 newmsg->flags |= EM_GO_AWAY;
361 newmsg->text = strdup("Automatic logoff requested.");
363 add_xmsg_to_context(ccptr, newmsg);
368 end_critical_section(S_SESSION_TABLE);
369 cprintf("%d Sent termination request to %d sessions.\n", CIT_OK, sessions);
373 // This is the back end for flush_conversations_to_disk()
374 // At this point we've isolated a single conversation (struct imlog)
375 // and are ready to write it to disk.
376 void flush_individual_conversation(struct imlog *im) {
377 struct CtdlMessage *msg;
379 char roomname[ROOMNAMELEN];
380 StrBuf *MsgBuf, *FullMsgBuf;
382 StrBufAppendBufPlain(im->conversation, HKEY(
388 MsgBuf = StrBufQuotedPrintableEncode(im->conversation);
389 FlushStrBuf(im->conversation);
390 FullMsgBuf = NewStrBufPlain(NULL, StrLength(im->conversation) + 100);
392 StrBufAppendBufPlain(FullMsgBuf, HKEY(
393 "Content-type: text/html; charset=UTF-8\r\n"
394 "Content-Transfer-Encoding: quoted-printable\r\n"
398 StrBufAppendBuf (FullMsgBuf, MsgBuf, 0);
401 msg = malloc(sizeof(struct CtdlMessage));
402 memset(msg, 0, sizeof(struct CtdlMessage));
403 msg->cm_magic = CTDLMESSAGE_MAGIC;
404 msg->cm_anon_type = MES_NORMAL;
405 msg->cm_format_type = FMT_RFC822;
406 if (!IsEmptyStr(im->usernames[0])) {
407 CM_SetField(msg, eAuthor, im->usernames[0]);
410 CM_SetField(msg, eAuthor, "Citadel");
412 if (!IsEmptyStr(im->usernames[1])) {
413 CM_SetField(msg, eRecipient, im->usernames[1]);
416 CM_SetField(msg, eOriginalRoom, PAGELOGROOM);
417 CM_SetAsFieldSB(msg, eMessageText, &FullMsgBuf); /* we own this memory now */
419 // Start with usernums[1] because it's guaranteed to be higher than usernums[0],
420 // so if there's only one party, usernums[0] will be zero but usernums[1] won't.
421 // Create the room if necessary. Note that we create as a type 5 room rather
422 // than 4, which indicates that it's a personal room but we've already supplied
423 // the namespace prefix.
425 // In the unlikely event that usernums[1] is zero, a room with an invalid namespace
426 // prefix will be created. That's ok because the auto-purger will clean it up later.
428 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[1], PAGELOGROOM);
429 CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
430 msgnum = CtdlSubmitMsg(msg, NULL, roomname);
433 // If there is a valid user number in usernums[0], save a copy for them too.
434 if (im->usernums[0] > 0) {
435 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[0], PAGELOGROOM);
436 CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
437 CtdlSaveMsgPointerInRoom(roomname, msgnum, 0, NULL);
440 // Finally, if we're logging instant messages globally, do that now.
441 if (!IsEmptyStr(CtdlGetConfigStr("c_logpages"))) {
442 CtdlCreateRoom(CtdlGetConfigStr("c_logpages"), 3, "", 0, 1, 1, VIEW_BBS);
443 CtdlSaveMsgPointerInRoom(CtdlGetConfigStr("c_logpages"), msgnum, 0, NULL);
448 // Locate instant message conversations which have gone idle
449 // (or, if the server is shutting down, locate *all* conversations)
450 // and flush them to disk (in the participants' log rooms, etc.)
451 void flush_conversations_to_disk(time_t if_older_than) {
453 struct imlog *flush_these = NULL;
454 struct imlog *dont_flush_these = NULL;
455 struct imlog *imptr = NULL;
456 struct CitContext *nptr;
459 nptr = CtdlGetContextArray(&nContexts) ; // Make a copy of the current wholist
461 begin_critical_section(S_IM_LOGS);
464 imlist = imlist->next;
466 // For a two party conversation, if one party has logged out, force flush.
468 int user0_is_still_online = 0;
469 int user1_is_still_online = 0;
470 for (i=0; i<nContexts; i++) {
471 if (nptr[i].user.usernum == imptr->usernums[0]) ++user0_is_still_online;
472 if (nptr[i].user.usernum == imptr->usernums[1]) ++user1_is_still_online;
474 if (imptr->usernums[0] != imptr->usernums[1]) { // two party conversation
475 if ((!user0_is_still_online) || (!user1_is_still_online)) {
476 imptr->lastmsg = 0L; // force flush
479 else { // one party conversation (yes, people do IM themselves)
480 if (!user0_is_still_online) {
481 imptr->lastmsg = 0L; // force flush
486 // Now test this conversation to see if it qualifies for flushing.
487 if ((time(NULL) - imptr->lastmsg) > if_older_than) {
488 // This conversation qualifies. Move it to the list of ones to flush.
489 imptr->next = flush_these;
493 // Move it to the list of ones not to flush.
494 imptr->next = dont_flush_these;
495 dont_flush_these = imptr;
498 imlist = dont_flush_these;
499 end_critical_section(S_IM_LOGS);
502 // We are now outside of the critical section, and we are the only thread holding a
503 // pointer to a linked list of conversations to be flushed to disk.
504 while (flush_these) {
506 flush_individual_conversation(flush_these); // This will free the string buffer
508 flush_these = flush_these->next;
514 void instmsg_timer(void) {
515 flush_conversations_to_disk(300); // Anything that hasn't peeped in more than 5 minutes
519 void instmsg_shutdown(void) {
520 flush_conversations_to_disk(0); // Get it ALL onto disk NOW.
524 // Initialization function, called from modules_init.c
525 char *ctdl_module_init_instmsg(void) {
527 CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
528 CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
529 CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
530 CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
531 CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC, PRIO_ASYNC + 1);
532 CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP, PRIO_STOP + 1);
533 CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
534 CtdlRegisterSessionHook(instmsg_timer, EVT_TIMER, PRIO_CLEANUP + 400);
535 CtdlRegisterSessionHook(instmsg_shutdown, EVT_SHUTDOWN, PRIO_SHUTDOWN + 10);
538 // return our module name for the log