]> Pileus Git - ~andy/lamechat/blobdiff - irc.c
Support pre-formatted text.
[~andy/lamechat] / irc.c
diff --git a/irc.c b/irc.c
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f99994ed8a54eb061e8ba9a44125ab490e70b7ac 100644 (file)
--- a/irc.c
+++ b/irc.c
@@ -0,0 +1,585 @@
+/*
+ * Copyright (C) 2017 Andy Spencer <andy753421@gmail.com>
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "util.h"
+#include "conf.h"
+#include "chat.h"
+#include "net.h"
+
+/* IRC constants */
+#define IRC_LINE 512
+
+/* IRC types */
+typedef enum {
+       IRC_CONNECT,
+       IRC_ENCRYPT,
+
+       IRC_SEND_TLS,
+       IRC_RECV_TLS,
+       IRC_SEND_STARTTLS,
+       IRC_RECV_STARTTLS,
+
+       IRC_SEND_SASL,
+       IRC_RECV_SASL,
+       IRC_SEND_PLAIN,
+       IRC_SEND_AUTH,
+       IRC_RECV_STATUS,
+       IRC_SEND_END,
+
+       IRC_SEND_USER,
+       IRC_SEND_NICK,
+       IRC_RECV_WELCOME,
+
+       IRC_JOIN,
+       IRC_READY,
+       IRC_DEAD,
+} irc_state_t;
+
+typedef struct {
+       user_t        user;
+
+       const char   *nick;
+} irc_user_t;
+
+typedef struct {
+       channel_t     channel;
+
+       const char   *dest;
+       int           join;
+} irc_channel_t;
+
+typedef struct {
+       server_t      server;
+
+       int           connect;
+       int           noverify;
+       const char   *host;
+       int           port;
+       int           tls;
+       const char   *nick;
+       const char   *auth;
+       const char   *pass;
+
+       irc_user_t    myself;
+       irc_channel_t system;
+       net_t         net;
+       irc_state_t   state;
+       char          line[IRC_LINE];
+       int           pos;
+} irc_server_t;
+
+typedef struct {
+       char src[IRC_LINE]; // (:([^ ]+) +)?
+       char cmd[IRC_LINE]; // (([A-Z0-9]+) +)
+       char dst[IRC_LINE]; // (([^ ]+)[= ]+)?
+       char arg[IRC_LINE]; // (([^: ]+) *)?
+       char msg[IRC_LINE]; // (:(.*))?
+       char from[IRC_LINE];
+} irc_command_t;
+
+/* Local functions */
+static void srv_notice(irc_server_t *srv, const char *fmt, ...)
+{
+       static char buf[1024];
+
+       va_list ap;
+       va_start(ap, fmt);
+       vsnprintf(buf, sizeof(buf), fmt, ap);
+       va_end(ap);
+
+       chat_recv(&srv->system.channel, NULL, buf);
+}
+
+static void chan_notice(irc_channel_t *chan, const char *fmt, ...)
+{
+       static char buf[1024];
+
+       va_list ap;
+       va_start(ap, fmt);
+       vsnprintf(buf, sizeof(buf), fmt, ap);
+       va_end(ap);
+
+       chat_recv(&chan->channel, NULL, buf);
+}
+
+static irc_channel_t *find_dest(irc_server_t *srv, const char *dest, int create)
+{
+       irc_channel_t *chan;
+
+       /* Find existing channels */
+       for (channel_t *cur = channels; cur; cur = cur->next) {
+               if (cur->server != &srv->server)
+                       continue;
+               chan = (irc_channel_t *)cur;
+               if (match(chan->dest, dest))
+                       return chan;
+       }
+
+       /* Create a new channel */
+       chan = new0(irc_channel_t);
+       chan->channel.server = &srv->server;
+       chan->channel.name = strcopy(dest);
+       chan->dest = strcopy(dest);
+       add_channel(&chan->channel);
+       return chan;
+}
+
+static irc_user_t *find_nick(irc_server_t *srv, const char *from, int create)
+{
+       irc_user_t *usr;
+
+       /* Find existing channels */
+       for (user_t *cur = users; cur; cur = cur->next) {
+               if (cur->server != &srv->server)
+                       continue;
+               usr = (irc_user_t *)cur;
+               if (match(usr->nick, from))
+                       return usr;
+       }
+
+       /* Create a new channel */
+       usr = new0(irc_user_t);
+       usr->user.server = &srv->server;
+       usr->user.name = strcopy(from);
+       usr->nick = strcopy(from);
+       add_user(&usr->user);
+       return usr;
+}
+
+static void join_channel(irc_server_t *srv, irc_channel_t *chan)
+{
+       chan_notice(chan, "Joining Channel: %s", chan->channel.name);
+       net_print(&srv->net, "JOIN %s\n",  chan->dest);
+       net_print(&srv->net, "TOPIC %s\n", chan->dest);
+       net_print(&srv->net, "WHO %s\n",   chan->dest);
+}
+
+static void part_channel(irc_server_t *srv, irc_channel_t *chan)
+{
+       chan_notice(chan, "Leaving Channel: %s", chan->channel.name);
+       net_print(&srv->net, "PART %s\n",  chan->dest);
+}
+
+static void parse_line(irc_server_t *srv, const char *line,
+                      irc_command_t *_cmd)
+{
+       int i;
+       const char *c = line;
+       char *src  = _cmd->src;
+       char *cmd  = _cmd->cmd;
+       char *dst  = _cmd->dst;
+       char *arg  = _cmd->arg;
+       char *msg  = _cmd->msg;
+       char *from = _cmd->from;
+
+       /* Clear strings */
+       src[0] = cmd[0] = dst[0] = arg[0] = msg[0] = from[0] = '\0';
+
+       /* Read src */
+       if (*c == ':') {
+               c++;
+               for (i = 0; *c && *c != ' '; i++)
+                       src[i] = *c++;
+               while (*c == ' ')
+                       c++;
+               src[i] = '\0';
+               for (i = 0; src[i] && src[i] != '!' && src[i] != ' '; i++)
+                       from[i] = src[i];
+               from[i] = '\0';
+       }
+
+       /* Read cmd */
+       for (i = 0; isalnum(*c); i++)
+               cmd[i] = *c++;
+       while (*c == ' ')
+               c++;
+       cmd[i] = '\0';
+
+       /* Read dst */
+       if ((*c && *c != ' ') &&
+           (strchr(c+1, ' ') || strchr(c+1, '='))) {
+               for (i = 0; *c && *c != ' '; i++)
+                       dst[i] = *c++;
+               while (*c == '=' || *c == ' ')
+                       c++;
+               dst[i] = '\0';
+       }
+
+       /* Read arg */
+       if  (*c && *c != ':' && *c != ' ') {
+               for (i = 0; *c && *c != ':'; i++)
+                       arg[i] = *c++;
+               while (*c == ' ')
+                       c++;
+               do
+                       arg[i--] = '\0';
+               while (i >= 0 && arg[i] == ' ');
+       }
+
+       /* Read msg */
+       if (*c == ':') {
+               c++;
+               for (i = 0; *c; i++)
+                       msg[i] = *c++;
+               msg[i] = '\0';
+       }
+
+       debug("got line: [%s]", line);
+       //debug("  src %s", src);
+       //debug("  cmd %s", cmd);
+       //debug("  dst %s", dst);
+       //debug("  arg %s", arg);
+       //debug("  msg %s", msg);
+}
+
+/* Callback functions */
+static void irc_run(irc_server_t *srv, const char *line);
+
+static void on_send(void *_srv)
+{
+       irc_server_t *srv = _srv;
+       irc_run(srv, "");
+}
+
+static void on_recv(void *_srv, char *buf, int len)
+{
+       irc_server_t *srv = _srv;
+
+       for (int i = 0; i < len; i++) {
+               if (srv->pos < IRC_LINE) {
+                       srv->line[srv->pos] = buf[i];
+                       srv->pos++;
+               }
+               if (buf[i] == '\n' || buf[i] == '\r') {
+                       srv->line[srv->pos-1] = '\0';
+                       if (srv->pos > 1)
+                               irc_run(srv, srv->line);
+                       srv->pos = 0;
+               }
+       }
+
+       irc_run(srv, "");
+}
+
+static void on_err(void *_srv, int errno)
+{
+       irc_server_t *srv = _srv;
+       irc_run(srv, "");
+}
+
+/* IRC State machine */
+static void irc_run(irc_server_t *srv, const char *line)
+{
+       static irc_command_t _cmd;
+       const char *cmd  = _cmd.cmd;
+       const char *dst  = _cmd.dst;
+       const char *arg  = _cmd.arg;
+       const char *msg  = _cmd.msg;
+       const char *from = _cmd.from;
+
+       /* Parse line */
+       parse_line(srv, line, &_cmd);
+
+       /* Connection Handling */
+       if (srv->state == IRC_CONNECT) {
+               srv->net.send = on_send;
+               srv->net.recv = on_recv;
+               srv->net.err  = on_err;
+               srv->net.data = srv;
+
+               srv_notice(srv, "Joining Server: %s", srv->server.name);
+               net_open(&srv->net, srv->host, srv->port);
+
+               if (srv->tls)
+                       srv->state = IRC_ENCRYPT;
+               else
+                       srv->state = IRC_SEND_TLS;
+       }
+
+       /* Start TLS */
+       if (srv->state == IRC_SEND_TLS) {
+               if (net_print(&srv->net, "CAP REQ :tls\n"))
+                       srv->state = IRC_RECV_TLS;
+       }
+       if (srv->state == IRC_RECV_TLS) {
+               if (match(cmd, "CAP") && match(arg, "ACK")) {
+                       srv_notice(srv, "Start TLS proceeding");
+                       srv->state = IRC_SEND_STARTTLS;
+               }
+               if (match(cmd, "CAP") && match(arg, "NAK")) {
+                       srv_notice(srv, "Start TLS unsupported");
+                       srv->state = IRC_SEND_END;
+               }
+       }
+       if (srv->state == IRC_SEND_STARTTLS) {
+               if (net_print(&srv->net, "STARTTLS\n"))
+                       srv->state = IRC_RECV_STARTTLS;
+       }
+       if (srv->state == IRC_RECV_STARTTLS) {
+               if (prefix(msg, "STARTTLS successful", NULL))
+                       srv->state = IRC_ENCRYPT;
+       }
+
+       /* Encryption */
+       if (srv->state == IRC_ENCRYPT) {
+               net_encrypt(&srv->net, srv->noverify ? NET_NOVERIFY : 0);
+               srv->state = IRC_SEND_SASL;
+       }
+
+       /* SASL authentication */
+       if (srv->state == IRC_SEND_SASL) {
+               if (net_print(&srv->net, "CAP REQ :sasl\n"))
+                       srv->state = IRC_RECV_SASL;
+       }
+       if (srv->state == IRC_RECV_SASL) {
+               if (match(cmd, "CAP") && match(arg, "ACK")) {
+                       srv_notice(srv, "SASL auth proceeding");
+                       srv->state = IRC_SEND_PLAIN;
+               }
+               if (match(cmd, "CAP") && match(arg, "NAK")) {
+                       srv_notice(srv, "SASL auth unsupported");
+                       srv->state = IRC_SEND_END;
+               }
+       }
+       if (srv->state == IRC_SEND_PLAIN) {
+               if (net_print(&srv->net, "AUTHENTICATE PLAIN\n"))
+                       srv->state = IRC_SEND_AUTH;
+       }
+       if (srv->state == IRC_SEND_AUTH) {
+               static char plain[IRC_LINE];
+               static char coded[IRC_LINE];
+               int len;
+               len = snprintf(plain, IRC_LINE, "%s%c%s%c%s",
+                               srv->auth, '\0', srv->auth, '\0', srv->pass);
+               len = base64(plain, len, coded, IRC_LINE);
+               if (net_print(&srv->net, "AUTHENTICATE %.*s\n", len, coded))
+                       srv->state = IRC_RECV_STATUS;
+       }
+       if (srv->state == IRC_RECV_STATUS) {
+               if (match(cmd, "903")) {
+                       srv_notice(srv, "SASL auth succeeded");
+                       srv->state = IRC_SEND_END;
+               }
+               if (match(cmd, "904") ||
+                   match(cmd, "905") ||
+                   match(cmd, "906") ||
+                   match(cmd, "907")) {
+                       srv_notice(srv, "SASL auth failed");
+                       srv->state = IRC_DEAD;
+               }
+       }
+       if (srv->state == IRC_SEND_END) {
+               if (net_print(&srv->net, "CAP END\n"))
+                       srv->state = IRC_SEND_USER;
+       }
+
+       /* Connection setup */
+       if (srv->state == IRC_SEND_USER) {
+               if (net_print(&srv->net, "USER %s %s %s :%s\n",
+                               getenv("USER") ?: "lameuser",
+                               get_hostname(), srv->host, srv->nick))
+                       srv->state = IRC_SEND_NICK;
+       }
+       if (srv->state == IRC_SEND_NICK) {
+               if (net_print(&srv->net, "NICK %s\n", srv->nick))
+                       srv->state = IRC_RECV_WELCOME;
+       }
+       if (srv->state == IRC_RECV_WELCOME) {
+               if (match(cmd, "001") && strstr(msg, "Welcome"))
+                       srv->state = IRC_JOIN;
+       }
+       if (srv->state == IRC_JOIN) {
+               for (channel_t *cur = channels; cur; cur = cur->next) {
+                       irc_channel_t *chan = (irc_channel_t*)cur;
+                       if (cur->server != &srv->server || !chan->join)
+                               continue;
+                       join_channel(srv, chan);
+               }
+               srv->state = IRC_READY;
+       }
+
+       /* Receive messages */
+       if (srv->state == IRC_READY) {
+               irc_channel_t *chan;
+               irc_user_t    *usr;
+               if (match(cmd, "PING")) {
+                       net_print(&srv->net, "PING %s\n", msg);
+               }
+               if (match(cmd, "TOPIC")) {
+                       chan = find_dest(srv, arg, 1);
+                       chan_notice(chan, "Topic changed to %s", msg);
+                       strset(&chan->channel.topic, msg);
+               }
+               if (match(cmd, "331") || match(cmd, "332")) {
+                       chan = find_dest(srv, arg, 1);
+                       chan_notice(chan, "Topic: %s", msg);
+                       strset(&chan->channel.topic, msg);
+               }
+               if (match(cmd, "353") && prefix(arg, "@", &arg)) {
+                       chan = find_dest(srv, arg, 1);
+                       chan_notice(chan, "Members: %s", msg);
+               }
+               if (match(cmd, "PRIVMSG") && dst[0] == '#') {
+                       chan = find_dest(srv, dst, 1);
+                       usr  = find_nick(srv, from, 1);
+                       chat_recv(&chan->channel, &usr->user, msg);
+               }
+               if (match(cmd, "PRIVMSG") && dst[0] != '#') {
+                       chan = find_dest(srv, from, 1);
+                       usr  = find_nick(srv, from, 1);
+                       chat_recv(&chan->channel, &usr->user, msg);
+               }
+       }
+
+       /* Receive notices */
+       if (match(cmd, "NOTICE") ||
+           match(cmd, "001")    ||
+           match(cmd, "372")    ||
+           match(cmd, "375")    ||
+           match(cmd, "376"))
+               srv_notice(srv, "%s", msg);
+}
+
+/* IRC functions */
+void irc_init(void)
+{
+       for (server_t *cur = servers; cur; cur = cur->next) {
+               if (cur->protocol != IRC)
+                       continue;
+
+               irc_server_t *srv = (irc_server_t*)cur;
+               srv->system.channel.server = &srv->server;
+               srv->system.channel.name   = srv->server.name;
+
+               if (!srv->port)
+                       srv->port = srv->tls ? 6697 : 6667;
+               if (!srv->nick)
+                       srv->nick = strcopy(getenv("USER"));
+               if (!srv->nick)
+                       srv->nick = strcopy("lameuser");
+               if (srv->connect)
+                       irc_run(srv, "");
+       }
+       for (channel_t *cur = channels; cur; cur = cur->next) {
+               if (cur->server->protocol != IRC)
+                       continue;
+
+               irc_channel_t *chan = (irc_channel_t*)cur;
+               if (!chan->dest)
+                       chan->dest = strcopy(cur->name);
+       }
+}
+
+void irc_config(server_t *server, channel_t *channel,
+                const char *group, const char *name,
+                const char *key, const char *value)
+{
+       irc_server_t  *srv  = (irc_server_t*)server;
+       irc_channel_t *chan = (irc_channel_t*)channel;
+
+       if (match(group, "server")) {
+               if (match(key, "protocol")) {
+                       irc_server_t *srv = new0(irc_server_t);
+                       srv->server.protocol = IRC;
+                       srv->server.name = strcopy(get_name(name));
+                       add_server(&srv->server);
+               }
+               else if (match(key, "connect"))
+                       srv->connect = get_bool(value);
+               else if (match(key, "noverify"))
+                       srv->noverify = get_bool(value);
+               else if (match(key, "host"))
+                       srv->host = get_string(value);
+               else if (match(key, "port"))
+                       srv->port = get_number(value);
+               else if (match(key, "tls"))
+                       srv->tls = get_bool(value);
+               else if (match(key, "nick"))
+                       srv->nick = get_string(value);
+               else if (match(key, "auth"))
+                       srv->auth = get_string(value);
+               else if (match(key, "pass"))
+                       srv->pass = get_string(value);
+       }
+       if (match(group, "channel")) {
+               if (match(key, "server")) {
+                       irc_channel_t *chan = new0(irc_channel_t);
+                       chan->channel.server = &srv->server;
+                       chan->channel.name = strcopy(get_name(name));
+                       add_channel(&chan->channel);
+               }
+               else if (match(key, "dest"))
+                       chan->dest = get_string(value);
+               else if (match(key, "join"))
+                       chan->join = get_bool(value);
+       }
+}
+
+void irc_send(channel_t *channel, const char *text)
+{
+       irc_channel_t *chan = (irc_channel_t*)channel;
+       irc_server_t  *srv  = (irc_server_t*)channel->server;
+       const char *arg;
+
+       if (text[0] == '/') {
+               if (prefix(text, "/join", &arg)) {
+                       if (arg)
+                               chan = find_dest(srv, arg, 1);
+                       if (chan)
+                               join_channel(srv, chan);
+               }
+               else if (prefix(text, "/part", &arg)) {
+                       if (arg)
+                               chan = find_dest(srv, arg, 0);
+                       if (chan)
+                               part_channel(srv, chan);
+               }
+               else if (prefix(text, "/query", &arg)) {
+                       if (!arg) {
+                               chan_notice(chan, "usage: /query <user>");
+                               return;
+                       }
+                       chan = find_dest(srv, arg, 0);
+                       chan_notice(chan, "User: %s", arg);
+               }
+               else {
+                       chan_notice(chan, "Unknown command %s", text);
+               }
+       } else {
+               if (chan == &srv->system) {
+                       chan_notice(chan, "Cannot send to server");
+               }
+               else if (!chan->dest) {
+                       chan_notice(chan, "No destination for message");
+               }
+               else {
+                       net_print(&srv->net, "PRIVMSG %s :%s\n", chan->dest, text);
+                       chat_recv(channel, &srv->myself.user, text);
+               }
+       }
+}
+
+void irc_exit(void)
+{
+}
+