From 7bcf0afcdb0263caa2fc6588284bb8dbfb198373 Mon Sep 17 00:00:00 2001 From: Aleteoryx Date: Sat, 26 Jul 2025 17:37:32 -0400 Subject: [PATCH] basic batch support Implements: https://todo.amehut.dev/~aleteoryx/sexchat/2 --- src/common/batch.c | 125 +++++++++++++++++++++++++++++++++++++++++ src/common/batch.h | 52 +++++++++++++++++ src/common/hexchat.h | 21 +++---- src/common/inbound.c | 1 + src/common/meson.build | 3 +- src/common/outbound.c | 4 +- src/common/proto-irc.c | 50 ++++++++++++----- src/common/proto-irc.h | 2 + 8 files changed, 231 insertions(+), 27 deletions(-) create mode 100644 src/common/batch.c create mode 100644 src/common/batch.h diff --git a/src/common/batch.c b/src/common/batch.c new file mode 100644 index 0000000000000000000000000000000000000000..165f400df7d88db0cb506f31d9e21238e5bf9fac --- /dev/null +++ b/src/common/batch.c @@ -0,0 +1,125 @@ +/* sexchat + * Copyright (C) 2025 Aleteoryx. + * + * 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 2 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* IRCv3 batch CAP */ + +#include + +#include "hexchat.h" +#include "batch.h" +#include "proto-irc.h" + +void +batch_new (ircconn *serv, char *reftag, char *type, char **args) +{ + struct batch *bat; + + if (batch_lookup (serv, reftag)) + return; + + if (strcmp (type, "soju.im/bouncer-networks") == 0) + { + bat = (struct batch *)g_new0 (struct batch_sojuim_bouncer_networks, 1); + bat->type = BATCH_SOJUIM_BOUNCER_NETWORKS; + } else + { + bat = (struct batch *)g_new0 (struct batch_replay, 1); + bat->type = BATCH_REPLAY; + } + + bat->reftag = g_strdup (reftag); + + serv->batches = g_slist_prepend (serv->batches, bat); +} + +void +batch_finalize (ircconn *serv, char *reftag) +{ + struct batch *bat; + + bat = batch_lookup (serv, reftag); + if (bat == NULL) + return; + + serv->batches = g_slist_remove (serv->batches, bat); + + switch (bat->type) + { + case BATCH_SOJUIM_BOUNCER_NETWORKS: + break; + + case BATCH_REPLAY: + { + struct batch_replay *bat2 = (struct batch_replay *)bat; + GSList *el; + char *line; + + bat2->messages = g_slist_reverse (bat2->messages); + for (el = bat2->messages; el; el = el->next) + { + line = (char *)el->data; + irc_inline (serv, line, strlen (line)); + g_free (line); + } + + g_slist_free (bat2->messages); + } + } + + g_free (bat); +} + +gboolean +batch_event (ircconn *serv, char *reftag, char *raw, char **word, char **word_eol) +{ + struct batch *bat; + + bat = batch_lookup (serv, reftag); + if (bat == NULL) + return FALSE; + + switch (bat->type) + { + case BATCH_SOJUIM_BOUNCER_NETWORKS: + break; + case BATCH_REPLAY: + { + struct batch_replay *bat2 = (struct batch_replay *)bat; + bat2->messages = g_slist_prepend (bat2->messages, g_strdup (raw)); + } + } + + return TRUE; +} + + +struct batch * +batch_lookup (ircconn *serv, char *reftag) +{ + GSList *el; + struct batch *bat; + + for (el = serv->batches; el; el = el->next) + { + bat = (struct batch *)el->data; + if (strcmp (bat->reftag, reftag) == 0) + return bat; + } + + return NULL; +} diff --git a/src/common/batch.h b/src/common/batch.h new file mode 100644 index 0000000000000000000000000000000000000000..ac1cd15533f11e9ae741f409e08fd48562cfcd27 --- /dev/null +++ b/src/common/batch.h @@ -0,0 +1,52 @@ +/* sexchat + * Copyright (C) 2025 Aleteoryx. + * + * 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 2 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "hexchat.h" + +#ifndef HEXCHAT_BATCH_H +#define HEXCHAT_BATCH_H + +enum { + BATCH_SOJUIM_BOUNCER_NETWORKS, // soju.im/bouncer-networks + BATCH_REPLAY, // fallback, no special handling other than being deferred till batch end +}; + +struct batch { + int type; + char *reftag; +}; + +struct batch_sojuim_bouncer_networks { + int type; // BATCH_SOJUIM_BOUNCER_NETWORKS + char *reftag; + GSList *networks; +}; + +struct batch_replay { + int type; // BATCH_REPLAY + char *reftag; + GSList *messages; +}; + + +void batch_new (ircconn *serv, char *reftag, char *type, char **args); +gboolean batch_event (ircconn *serv, char *reftag, char *raw, char **word, char **word_eol); +void batch_finalize (ircconn *serv, char *reftag); +struct batch *batch_lookup (ircconn *serv, char *reftag); + +#endif diff --git a/src/common/hexchat.h b/src/common/hexchat.h index edaae58eb8489f9e06ca952779903e27da5c20ab..de8958afebc68b5a16dae82a27f9d22f14c8c30b 100644 --- a/src/common/hexchat.h +++ b/src/common/hexchat.h @@ -530,24 +530,25 @@ typedef struct server struct session *front_session; /* front-most window/tab */ struct session *server_session; /* server window/tab */ - struct server_gui *gui; /* initialized by fe_new_server */ + struct server_gui *gui; /* initialized by fe_new_server */ - unsigned int ctcp_counter; /*flood */ + unsigned int ctcp_counter; /*flood */ time_t ctcp_last_time; - unsigned int msg_counter; /*counts the msg tab opened in a certain time */ + unsigned int msg_counter; /*counts the msg tab opened in a certain time */ time_t msg_last_time; - /*time_t connect_time;*/ /* when did it connect? */ - unsigned long lag_sent; /* we are still waiting for this ping response*/ - time_t ping_recv; /* when we last got a ping reply */ - time_t away_time; /* when we were marked away */ + unsigned long lag_sent; /* we are still waiting for this ping response*/ + time_t ping_recv; /* when we last got a ping reply */ + time_t away_time; /* when we were marked away */ char *encoding; - GIConv read_converter; /* iconv converter for converting from server encoding to UTF-8. */ - GIConv write_converter; /* iconv converter for converting from UTF-8 to server encoding. */ + GIConv read_converter; /* iconv converter for converting from server encoding to UTF-8. */ + GIConv write_converter; /* iconv converter for converting from UTF-8 to server encoding. */ - GSList *favlist; /* list of channels & keys to join */ + GSList *favlist; /* list of channels & keys to join */ + + GSList *batches; /* list of batches */ unsigned int motd_skipped:1; unsigned int connected:1; diff --git a/src/common/inbound.c b/src/common/inbound.c index 2eb52a443dc90793d4382259037ac99c52b9248a..1c7cfe9361cee1ddfa8c69470b1b1d6fbd9fc054 100644 --- a/src/common/inbound.c +++ b/src/common/inbound.c @@ -1716,6 +1716,7 @@ static const char * const supported_caps[] = { "extended-monitor", "message-tags", "echo-message", + "batch", /* ZNC */ "znc.in/server-time-iso", diff --git a/src/common/meson.build b/src/common/meson.build index 35db2c27329e361acd6bb327eb79a7aeba34c541..5d99588cbffdc59ce3f218fa40c27539f834a14d 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -1,4 +1,5 @@ common_sources = [ + 'batch.c', 'cfgfiles.c', 'chanopt.c', 'ctcp.c', @@ -18,7 +19,7 @@ common_sources = [ 'scram.c', 'server.c', 'servlist.c', - 'text.c', + 'text.c', 'tree.c', 'url.c', 'userlist.c', diff --git a/src/common/outbound.c b/src/common/outbound.c index 4d1993c6b51744f85d2b74df89862ed2a6660c79..a52625f6a09c87f152ee09e308257691e9b3cc3c 100644 --- a/src/common/outbound.c +++ b/src/common/outbound.c @@ -151,8 +151,8 @@ server_sendquit (session * sess) void process_data_init (char *buf, char *cmd, char *word[], - char *word_eol[], gboolean handle_quotes, - gboolean allow_escape_quotes) + char *word_eol[], gboolean handle_quotes, + gboolean allow_escape_quotes) { int wordcount = 2; int space = FALSE; diff --git a/src/common/proto-irc.c b/src/common/proto-irc.c index d807e008b29e12df5b6bc4be3d40b3af190133f4..7d708c3090570118bf513f7caf7a26196220b9e0 100644 --- a/src/common/proto-irc.c +++ b/src/common/proto-irc.c @@ -44,6 +44,7 @@ #include "hexchatc.h" #include "url.h" #include "servlist.h" +#include "batch.h" static void irc_login (server *serv, char *user, char *realname) @@ -1103,14 +1104,14 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[], } return; - case WORDL('P', 'I', 'N', 'G'): + case WORDL('P','I','N','G'): tcp_sendf (sess->server, "PONG %s\r\n", word_eol[3]); return; case WORDL('P','O','N','G'): inbound_ping_reply (serv->server_session, - (word[4][0] == ':') ? word[4] + 1 : word[4], - word[3], tags_data); + (word[4][0] == ':') ? word[4] + 1 : word[4], + word[3], tags_data); return; case WORDL('Q','U','I','T'): @@ -1175,16 +1176,16 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[], inbound_account (serv, nick, STRIP_COLON(word, word_eol, 3), tags_data); return; - case WORDL('A', 'U', 'T', 'H'): + case WORDL('A','U','T','H'): inbound_sasl_authenticate (sess->server, word_eol[3]); return; - case WORDL('C', 'H', 'G', 'H'): + case WORDL('C','H','G','H'): inbound_user_info (sess, NULL, word[3], STRIP_COLON(word, word_eol, 4), NULL, nick, NULL, NULL, 0xff, tags_data); return; - case WORDL('S', 'E', 'T', 'N'): + case WORDL('S','E','T','N'): inbound_user_info (sess, NULL, NULL, NULL, NULL, nick, STRIP_COLON(word, word_eol, 3), NULL, 0xff, tags_data); return; @@ -1310,8 +1311,8 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[], case WORDL('T','O','P','I'): inbound_topicnew (serv, nick, word[3], - (word_eol[4][0] == ':') ? word_eol[4] + 1 : word_eol[4], - tags_data); + (word_eol[4][0] == ':') ? word_eol[4] + 1 : word_eol[4], + tags_data); return; case WORDL('W','A','L','L'): @@ -1320,6 +1321,13 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[], text++; EMIT_SIGNAL_TAGS (XP_TE_WALLOPS, sess, nick, text, NULL, NULL, 0, tags_data); return; + + case WORDL('B','A','T','C'): + if (word[3][0] == '-') + batch_finalize (serv, word[3]+1); + else + batch_new (serv, word[3]+1, word[4], word+5); + return; } } @@ -1372,8 +1380,8 @@ garbage: /* handle named messages that DON'T start with a ':' */ static void -process_named_servermsg (session *sess, char *buf, char *rawname, char *word_eol[], - const message_tags_data *tags_data) +process_named_servermsg (session *sess, char *buf, char *rawname, char *word[], char *word_eol[], + const message_tags_data *tags_data) { sess = sess->server->server_session; @@ -1400,6 +1408,14 @@ process_named_servermsg (session *sess, char *buf, char *rawname, char *word_eol inbound_sasl_authenticate (sess->server, word_eol[2]); return; } + if (!strncmp (buf, "BATCH ", 6)) + { + if (word[3][0] == '-') + batch_finalize (sess->server, word[3]+1); + else + batch_new (sess->server, word[3]+1, word[4], word+5); + return; + } EMIT_SIGNAL_TAGS (XP_TE_SERVTEXT, sess, buf, sess->server->servername, rawname, NULL, 0, tags_data); } @@ -1503,7 +1519,7 @@ handle_message_tag_time (const char *time, message_tags_data *tags_data) */ static void handle_message_tags (server *serv, const char *tags_str, - message_tags_data *tags_data) + message_tags_data *tags_data) { char **tags; int i; @@ -1527,8 +1543,11 @@ handle_message_tags (server *serv, const char *tags_str, if (serv->have_account_tag && !strcmp (key, "account")) tags_data->account = g_strdup (value); - if (serv->have_message_tags && !strcmp (key, "msgid")) + if (!strcmp (key, "msgid")) tags_data->msgid = g_strdup (value); + + if (!strcmp (key, "batch")) + tags_data->batchref = g_strdup (value); if (serv->have_idmsg && strcmp (key, "solanum.chat/identified")) tags_data->identified = TRUE; @@ -1577,6 +1596,9 @@ irc_inline (server *serv, char *buf, int len) /* split line into words and words_to_end_of_line */ process_data_init (pdibuf, buf, word, word_eol, FALSE, FALSE); + + if (tags_data.batchref && batch_event (serv, tags_data.batchref, buf, word, word_eol)) + goto xit; if (buf[0] == ':') { @@ -1595,7 +1617,7 @@ irc_inline (server *serv, char *buf, int len) word_eol[1] = buf; /* keep the ":" for plugins */ if (plugin_emit_server (sess, type, word, word_eol, - tags_data.timestamp)) + tags_data.timestamp)) goto xit; word[1]++; @@ -1612,7 +1634,7 @@ irc_inline (server *serv, char *buf, int len) if (buf[0] != ':') { - process_named_servermsg (sess, buf, word[0], word_eol, &tags_data); + process_named_servermsg (sess, buf, word[0], word, word_eol, &tags_data); goto xit; } diff --git a/src/common/proto-irc.h b/src/common/proto-irc.h index 7467d8dc912d1b8d85d73f3f2a7d75f34a20cbc0..fad32d5e29d59475fd40fecd52a7f7a2dfd33791 100644 --- a/src/common/proto-irc.h +++ b/src/common/proto-irc.h @@ -27,6 +27,7 @@ { \ NULL, /* account name */ \ NULL, /* message id */ \ + NULL, /* batch ref */ \ FALSE, /* identified to nick */ \ (time_t)0, /* timestamp */ \ } @@ -41,6 +42,7 @@ typedef struct { char *account; char *msgid; + char *batchref; gboolean identified; time_t timestamp; } message_tags_data;