hirc

IRC client
Log | Files | Refs

commit 4a0305df5ec381fc60be65da6bca10472823594d
Author: hhvn <dev@hhvn.uk>
Date:   Sat, 23 Oct 2021 22:54:51 +0100

INIT: probably should have done this before. Basic UI.

Diffstat:
AFORMAT | 4++++
AMakefile | 22++++++++++++++++++++++
Achan.c | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.h | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahandle.c | 351+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahirc.h | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahist.c | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain.c | 239+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anick.c | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserv.c | 328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astruct.h | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aui.c | 240+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 2124 insertions(+), 0 deletions(-)

diff --git a/FORMAT b/FORMAT @@ -0,0 +1,4 @@ +<line> ::= [<timestamp> ' '] [<prefix> ' '] <msg> +<timestamp> ::= '!' <time> +<time> ::= (string representation of long long) +<prefix> ::= ':' <servername> | ':' <nick> ['!' <user>] ['@' <host>] diff --git a/Makefile b/Makefile @@ -0,0 +1,22 @@ +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin +BIN = hirc +OBJ = main.o handle.o hist.o nick.o chan.o serv.o ui.o + +# Comment to disable TLS +LDTLS = -ltls +CTLS = -DTLS + +CFLAGS = -g -O0 $(CTLS) +LDFLAGS = -lncurses $(LDTLS) + +$(BIN): $(OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJ) + +clean: + -rm -f $(OBJ) + +.c.o: + $(CC) $(CFLAGS) -c $< + +$(OBJ): config.h diff --git a/chan.c b/chan.c @@ -0,0 +1,132 @@ +#include <stdlib.h> +#include <string.h> +#include "hirc.h" + +void +chan_free(struct Channel *channel) { + if (channel) { + free(channel->name); + free(channel->mode); + nick_free_list(&channel->nicks); + free(channel->nicks); + hist_free_list(channel->history); + free(channel); + } +} + +void +chan_free_list(struct Channel **head) { + struct Channel *p; + + if (!head || !*head) + return; + + for (p = (*head)->next; p; p = p->next) + chan_free(p->prev); + *head = NULL; +} + +struct Channel * +chan_create(struct Server *server, char *name) { + struct Channel *channel; + + channel = emalloc(sizeof(struct Channel)); + channel->name = name ? estrdup(name) : NULL; + channel->next = channel->prev = NULL; + channel->nicks = NULL; + channel->old = 0; + channel->server = server; + channel->history = emalloc(sizeof(struct HistInfo)); + channel->history->activity = Activity_ignore; + channel->history->unread = 0; + channel->history->server = server; + channel->history->channel = channel; + channel->history->history = NULL; + + return channel; +} + +int +chan_selected(struct Channel *channel) { + if (selected_channel == channel) + return 1; + else + return 0; +} + +struct Channel * +chan_add(struct Server *server, struct Channel **head, char *name) { + struct Channel *channel, *p; + + if (!name) + return NULL; + + if ((channel = chan_create(server, name)) == NULL) + return NULL; + + if (!*head) { + *head = channel; + return channel; + } + + p = *head; + for (; p && p->next; p = p->next); + p->next = channel; + channel->prev = p; + + return channel; +} + +struct Channel * +chan_get(struct Channel **head, char *name, int old) { + struct Channel *p; + + /* if old is negative, match regardless of p->old + * else return only when p->old and old match */ + + if (!head || !*head || !name) + return NULL; + + for (p = *head; p; p = p->next) { + if (strcmp(p->name, name) == 0 && (old < 0 || p->old == old)) + return p; + } + + return NULL; +} + +int +chan_isold(struct Channel *channel) { + if (channel) + return channel->old; + else + return 0; +} + +void +chan_setold(struct Channel *channel, int old) { + channel->old = old; +} + +int +chan_remove(struct Channel **head, char *name) { + struct Channel *p; + + if (!head || !name) + return -1; + + if ((p = chan_get(head, name, -1)) == NULL) + return 0; + + if (p->prev == NULL) { + *head = p->next; + chan_free(p); + return 1; + } + + p->prev->next = p->next; + if (p->next != NULL) + p->next->prev = p->prev; + chan_free(p); + return 1; +} diff --git a/config.h b/config.h @@ -0,0 +1,151 @@ +#ifndef H_CONFIG +#define H_CONFIG + +#include "struct.h" + +#define MAIN_NICK "hhvn" +#define MAIN_USERNAME "Fanatic" +#define MAIN_REALNAME "gopher://hhvn.uk" + +static char *logdir = "~/.local/hirc"; +static struct Netconfig netconfig[] = { + { + .name = "test", + .host = "localhost", + .port = "6667", + .nick = MAIN_NICK, + .user = MAIN_USERNAME, + .real = MAIN_REALNAME, + .join = { "#test", "#test2", NULL }, + .tls = 0, + .tls_verify = 0, + }, + /* + { + .name = "hlircnet", + .host = "irc.hhvn.uk", + .port = "6667", + .nick = MAIN_NICK, + .user = MAIN_USERNAME, + .real = MAIN_REALNAME, + .join = { + "#hlircnet", "#help", "#gopher", "#vhosts", + "#hlfm", "#cgo", "#distrotube", NULL + }, + }, + { + .name = "dataswamp", + .host = "irc.dataswamp.org", + .port = "6667", + .nick = MAIN_NICK, + .user = MAIN_USERNAME, + .real = MAIN_REALNAME, + .join = { "#dataswamp", NULL }, + }, + { + .name = "efnet", + .host = "efnet.port80.se", + .port = "6667", + .nick = MAIN_NICK, + .user = MAIN_USERNAME, + .real = MAIN_REALNAME, + .join = { "#asciiart", "#LRH", "#thepiratebay.org", NULL }, + }, + { + .name = "ikteam", + .host = "irc.nk.ax", + .port = "6667", + .nick = MAIN_NICK, + .user = MAIN_USERNAME, + .real = MAIN_REALNAME, + .join = { "#chat", NULL }, + }, + { + .name = "nebulacentre", + .host = "irc.nebulacentre.net", + .port = "6667", + .nick = MAIN_NICK, + .user = MAIN_USERNAME, + .real = MAIN_REALNAME, + .join = { "#general", NULL }, + }, + { + .name = "sdf", + .host = "irc.sdf.org", + .port = "6667", + .nick = MAIN_NICK, + .user = MAIN_USERNAME, + .real = MAIN_REALNAME, + .join = { "#sdf", "#gopher", "#helpdesk", NULL }, + } + */ +}; + +/* real maximum = MAX_HISTORY * (channels + servers + queries) */ +#define MAX_HISTORY 1024 + +static unsigned short colourmap[] = { + /* original 16 mirc colours + * some clients use the first 16 ansi colours for this, + * but here I use the 256 colours to ensure terminal-agnosticism */ + [0] = 255, 16, 19, 46, 124, 88, 127, 184, + [8] = 208, 46, 45, 51, 21, 201, 240, 255, + + /* extended */ + [16] = 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89, + [28] = 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125, + [40] = 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161, + [52] = 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198, + [64] = 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205, + [76] = 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212, + [88] = 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, + + /* transparency */ + [99] = -1 +}; + +/* (mIRC) colour for any messages sent by oneself */ +static unsigned short selfcolour = 90; + +/* (mIRC) inclusive colour range for any messages sent + * by others. Use same number twice for constant colour */ +static unsigned short othercolour[2] = {28, 63}; + +/* default channel types */ +static char *default_chantypes = "#&!+"; + +/* default prefixes/priveledges, (symbols)modes */ +static char *default_prefixes = "(@+)ov"; + +/* send ping to server after n seconds of inactivity */ +static int pinginact = 200; + +/* max seconds to wait between reconnects */ +static long maxreconnectinterval = 600; + +/* number of seconds between reconnects, + * multiplied by amount of failed reconnects. + * Example: reconnectinterval = 10, maxreconnectinterval = 600 + * 1st: 0 * 10 = 0 + * 2nd: 1 * 10 = 10 + * 3rd: 2 * 10 = 20 + * 10th: 10 * 10 = 100 + * 60th: 60 * 10 = 600 + * 61st: 600, maxreconnectinterval reached */ +static long reconnectinterval = 10; + +/* nicklist location: + * HIDDEN, LEFT, RIGHT */ +static short nicklistlocation = RIGHT; + +/* width of nicklist in columns */ +static int nicklistwidth = 15; + +/* window list location: + * HIDDEN, LEFT, RIGHT */ +static short winlistlocation = LEFT; + +/* width of window list in columns */ +static int winlistwidth = 25; + +#endif /* H_CONFIG */ diff --git a/handle.c b/handle.c @@ -0,0 +1,351 @@ +#include <stdio.h> +#include <ctype.h> +#include <string.h> +#include <stdlib.h> +#include "hirc.h" + +struct Handler handlers[] = { + { "PING", handle_PING }, + { "JOIN", handle_JOIN }, + { "PART", handle_PART }, + { "QUIT", handle_QUIT }, + { "NICK", handle_NICK }, + { "PRIVMSG", handle_PRIVMSG }, + { "NOTICE", handle_PRIVMSG }, + { "005", handle_ISUPPORT }, + { "353", handle_NAMREPLY }, + { "433", handle_NICKNAMEINUSE }, + { NULL, NULL }, +}; + +void +handle_PING(char *msg, char **params, struct Server *server, time_t timestamp) { + if (**params == ':') + params++; + + if (param_len(params) < 2) + return; + + ircprintf(server, "PONG :%s\r\n", *(params+1)); +} + +void +chan_printlist(struct Server *server) { + struct Channel *channel; + struct Nick *nick; + + printf("---\n"); + for (channel = server->channels; channel; channel = channel->next) { + if (chan_isold(channel)) + printf("%s (old)", channel->name); + else + printf("%s: ", channel->name); + for (nick = channel->nicks; nick; nick = nick->next) { + if (nick_isself(nick)) + printf("%c%s(me) ", nick->priv, nick->nick); + else + printf("%c%s ", nick->priv, nick->nick); + } + printf("\n"); + } + for (channel = server->privs; channel; channel = channel->next) { + if (chan_isold(channel)) + printf("%s (old\n)", channel->name); + else + printf("%s\n", channel->name); + } + printf("---\n"); +} + +void +handle_JOIN(char *msg, char **params, struct Server *server, time_t timestamp) { + struct Channel *chan; + struct Nick *nick; + char *target; + + if (**params != ':' || param_len(params) < 3) + return; + + target = *(params+2); + if ((chan = chan_get(&server->channels, target, -1)) == NULL) + chan = chan_add(server, &server->channels, target); + chan_setold(chan, 0); + + nick = nick_create(*params, ' ', server); + if (nick_get(&chan->nicks, nick->nick) == NULL) + nick_add(&chan->nicks, *params, ' ', server); + + hist_add(server, server->history, nick, msg, params, Activity_status, timestamp, HIST_LOG); + hist_add(server, chan->history, nick, msg, params, Activity_status, timestamp, HIST_SHOW); + nick_free(nick); + + chan_printlist(server); +} + +void +handle_PART(char *msg, char **params, struct Server *server, time_t timestamp) { + struct Channel *chan; + struct Nick *nick; + char *target; + + if (**params != ':' || param_len(params) < 3) + return; + + target = *(params+2); + if ((chan = chan_get(&server->channels, target, -1)) == NULL) + chan = chan_add(server, &server->channels, target); + + nick = nick_create(*params, ' ', server); + if (nick_isself(nick)) { + chan_setold(chan, 1); + nick_free_list(&chan->nicks); + } else { + nick_remove(&chan->nicks, nick->nick); + } + + hist_add(server, server->history, nick, msg, params, Activity_status, timestamp, HIST_LOG); + hist_add(server, chan->history, nick, msg, params, Activity_status, timestamp, HIST_SHOW); + nick_free(nick); + + chan_printlist(server); +} + +void +handle_QUIT(char *msg, char **params, struct Server *server, time_t timestamp) { + struct Channel *chan; + struct Nick *nick; + + if (**params != ':' || param_len(params) < 2) + return; + + nick = nick_create(*params, ' ', server); + if (nick_isself(nick)) { + /* TODO: umm, sound like a big deal anyone? */ + (void)0; + } + + hist_add(server, server->history, nick, msg, params, Activity_status, timestamp, HIST_LOG); + for (chan = server->channels; chan; chan = chan->next) { + if (nick_get(&chan->nicks, nick->nick) != NULL) { + nick_remove(&chan->nicks, nick->nick); + hist_add(server, chan->history, nick, msg, params, Activity_status, timestamp, HIST_SHOW); + } + } + + nick_free(nick); + + chan_printlist(server); +} + +void +handle_PRIVMSG(char *msg, char **params, struct Server *server, time_t timestamp) { + int act_direct = Activity_hilight, act_regular = Activity_message; + struct Channel *chan; + struct Channel *priv; + struct Nick *nick; + char *target; + + if (**params != ':' || param_len(params) < 4) + return; + + if (strcmp(*params, "NOTICE") == 0) + act_direct = act_regular = Activity_notice; + + target = *(params + 2); + nick = nick_create(*params, ' ', server); + if (strchr(nick->nick, '.')) { + /* it's a server */ + hist_add(server, server->history, NULL, msg, params, Activity_status, timestamp, HIST_DFL); + } else if (strcmp(target, server->self->nick) == 0) { + /* it's messaging me */ + if ((priv = chan_get(&server->privs, nick->nick, -1)) == NULL) + priv = chan_add(server, &server->privs, nick->nick); + chan_setold(priv, 0); + + hist_add(server, priv->history, nick, msg, params, act_direct, timestamp, HIST_DFL); + } else if (nick_isself(nick) && !chrcmp(*target, "#&!+")) { + /* i'm messaging someone */ + if ((priv = chan_get(&server->privs, target, -1)) == NULL) + priv = chan_add(server, &server->privs, target); + chan_setold(priv, 0); + + hist_add(server, priv->history, nick, msg, params, act_regular, timestamp, HIST_DFL); + } else { + /* message to a channel */ + if ((chan = chan_get(&server->channels, target, -1)) == NULL) + chan = chan_add(server, &server->channels, target); + + hist_add(server, chan->history, nick, msg, params, act_regular, timestamp, HIST_DFL); + } + + nick_free(nick); + chan_printlist(server); + hist_print_server(server); +} + +void +handle_ISUPPORT(char *msg, char **params, struct Server *server, time_t timestamp) { + char *key, *value; + + hist_add(server, server->history, NULL, msg, params, Activity_status, timestamp, HIST_DFL); + if (**params == ':') + params++; + + if (param_len(params) < 4) + return; + + params += 2; + + /* skip the last param ".... :are supported by this server" */ + for (; *params && *(params+1); params++) { + key = *params; + if ((value = strchr(key, '=')) != NULL) { + *value = '\0'; + if (*(value+1)) + value++; + else + value = NULL; + } + + support_set(server, key, value); + } +} + +void +handle_NAMREPLY(char *msg, char **params, struct Server *server, time_t timestamp) { + struct Channel *chan; + char *nick, priv, *target; + char **nicks, **nicksref; + char *supportedprivs; + + if (**params == ':') + params++; + + if (param_len(params) < 5) + return; + + params += 3; + + target = *params; + if ((chan = chan_get(&server->channels, target, -1)) == NULL) + chan = chan_add(server, &server->channels, target); + params++; + + supportedprivs = struntil(support_get(server, "PREFIX"), ')') + 1; + + nicksref = nicks = param_create(*params); + for (; *nicks && **nicks; nicks++) { + priv = ' '; + nick = *nicks; + if (chrcmp(**nicks, supportedprivs)) { + priv = **nicks; + while (chrcmp(*nick, supportedprivs)) + nick++; + } + if (nick_get(&chan->nicks, nick) != NULL) + nick_remove(&chan->nicks, nick); + nick_add(&chan->nicks, nick, priv, server); + } + + param_free(nicksref); + + chan_printlist(server); +} + +void +handle_NICKNAMEINUSE(char *msg, char **params, struct Server *server, time_t timestamp) { + char nick[64]; /* should be limited to 9 chars, but newer servers *shrug*/ + + hist_add(server, server->history, NULL, msg, params, Activity_status, timestamp, HIST_DFL); + snprintf(nick, sizeof(nick), "%s_", server->self->nick); + nick_free(server->self); + server->self = nick_create(nick, ' ', server); + server->self->self = 1; + ircprintf(server, "NICK %s\r\n", nick); +} + +void +handle_NICK(char *msg, char **params, struct Server *server, time_t timestamp) { + struct Nick *nick, *chnick; + struct Channel *chan; + char *newnick; + + if (**params != ':' || !*(params+1) || !*(params+2)) + return; + + nick = nick_create(*params, ' ', server); + hist_add(server, server->history, nick, msg, params, Activity_status, timestamp, HIST_DFL); + newnick = *(params+2); + + if (strcmp(nick->nick, newnick) == 0) + return; + + if (nick_isself(nick)) { + nick_free(server->self); + server->self = nick_create(newnick, ' ', server); + server->self->self = 1; + } + + for (chan = server->channels; chan; chan = chan->next) { + if ((chnick = nick_get(&chan->nicks, nick->nick)) != NULL) { + nick_add(&chan->nicks, newnick, chnick->priv, server); + nick_remove(&chan->nicks, nick->nick); + } + } +} + +void +handle(int rfd, struct Server *server) { + time_t timestamp; + char **params; + char *cmd; + char *msg; + char buf[511]; + /* using a buffer size of 511: + * - RFC1459 defines the maximum size of a message to be 512 + * - read_line() doesn't copy the \r\n so this is reduced to 510 + * - a \0 is needed at the end, so 510 + 1 = 511 */ + int i; + + if (!read_line(rfd, buf, sizeof(buf))) { + if (buf[0] == EOF || buf[0] == 3 || buf[0] == 4) { + serv_disconnect(server, 1); + hist_format(server, server->history, Activity_error, HIST_SHOW, + "SELF_CONNECTLOST %s %s %s :EOF received", + server->name, server->host, server->port); + } + return; + } + msg = buf; + + if (*msg == '!' && strchr(msg, ' ') && *(strchr(msg, ' ')+1) && *(msg+1) != ' ') { + msg++; + timestamp = (time_t)strtoll(msg, NULL, 10); + msg = strchr(msg, ' ') + 1; + } else { + timestamp = time(NULL); + } + + params = param_create(msg); + if (!*params) { + free(params); + return; + } + + if (**params == ':' || **params == '|') + cmd = *(params + 1); + else + cmd = *(params); + + for (i=0; cmd && handlers[i].cmd; i++) { + if (strcmp(handlers[i].cmd, cmd) == 0) { + if (handlers[i].func) + handlers[i].func(msg, params, server, timestamp); + /* NULL handlers will stop a message being added to server->history */ + return; + } + } + + /* add it to server->history if there is no handler */ + hist_add(server, server->history, NULL, msg, params, Activity_status, timestamp, HIST_DFL); +} diff --git a/hirc.h b/hirc.h @@ -0,0 +1,117 @@ +#ifndef H_HIRC +#define H_HIRC + +#include "struct.h" +#include "config.h" +#define PARAM_MAX 64 + +/* main.c */ +void * emalloc(size_t size); +char * estrdup(const char *str); +void param_free(char **params); +int param_len(char **params); +char ** param_create(char *msg); +int read_line(int fd, char *buf, size_t buf_len); +int ircprintf(struct Server *server, char *format, ...); +char * homepath(char *path); +char chrcmp(char c, char *s); +char * struntil(char *str, char until); + +/* chan.c */ +void chan_free(struct Channel *channel); +void chan_free_list(struct Channel **head); +struct Channel *chan_create(struct Server *server, char *name); +struct Channel *chan_get(struct Channel **head, char *name, int old); +struct Channel *chan_add(struct Server *server, struct Channel **head, char *name); +int chan_isold(struct Channel *channel); +void chan_setold(struct Channel *channel, int old); +/* struct Channel *chan_dup(struct Channel *channel); */ +int chan_remove(struct Channel **head, char *name); +int chan_selected(struct Channel *channel); + +/* nick.c */ +void prefix_tokenize(char *prefix, char **nick, char **ident, char **host); +void nick_free(struct Nick *nick); +void nick_free_list(struct Nick **head); +struct Nick * nick_create(char *prefix, char priv, struct Server *server); +struct Nick * nick_get(struct Nick **head, char *nick); +struct Nick * nick_add(struct Nick **head, char *prefix, char priv, struct Server *server); +struct Nick * nick_dup(struct Nick *nick, struct Server *server); +int nick_isself(struct Nick *nick); +int nick_isself_server(struct Nick *nick, struct Server *server); +int nick_remove(struct Nick **head, char *nick); +char * nick_strprefix(struct Nick *nick); + +/* hist.c */ +void hist_free(struct History *history); +void hist_free_list(struct HistInfo *histinfo); +struct History *hist_create(struct Server *server, struct Nick *from, char *msg, + char **params, enum Activity activity, time_t timestamp, enum HistOpt options); +struct History *hist_add(struct Server *server, struct HistInfo *histinfo, + struct Nick *from, char *msg, char **params, enum Activity activity, + time_t timestamp, enum HistOpt options); +struct History *hist_format(struct Server *server, struct HistInfo *history, + enum Activity activity, enum HistOpt options, char *format, ...); +int hist_len(struct History **history); +void hist_print(struct HistInfo *histinfo, char *prefix); /* debug */ +void hist_print_server(struct Server *server); /* debug */ +int hist_log(char *msg, struct Nick *from, time_t timestamp, struct Server *server); + +/* serv.c */ +void serv_free(struct Server *server); +void serv_connect(struct Server *server); +struct Server * serv_create(char *name, char *host, char *port, char *nick, + char *username, char *realname, int tls, int tls_verify); +struct Server * serv_get(struct Server **head, char *name); +struct Server * serv_get_byrfd(struct Server **head, int rfd); +struct Server * serv_add(struct Server **head, char *name, char *host, + char *port, char *nick, char *username, char *realname, int tls, int tls_verify); +int serv_len(struct Server **head); +int serv_poll(struct Server **head, int timeout); +int serv_remove(struct Server **head, char *name); +int serv_selected(struct Server *server); +void serv_disconnect(struct Server *server, int reconnect); +char * support_get(struct Server *server, char *key); +void support_set(struct Server *server, char *key, char *value); + +/* handle.c */ +void handle(int rfd, struct Server *server); +void handle_PING(char *msg, char **params, struct Server *server, time_t timestamp); +void handle_JOIN(char *msg, char **params, struct Server *server, time_t timestamp); +void handle_PART(char *msg, char **params, struct Server *server, time_t timestamp); +void handle_QUIT(char *msg, char **params, struct Server *server, time_t timestamp); +void handle_PRIVMSG(char *msg, char **params, struct Server *server, time_t timestamp); +void handle_ISUPPORT(char *msg, char **params, struct Server *server, time_t timestamp); +void handle_NAMREPLY(char *msg, char **params, struct Server *server, time_t timestamp); +void handle_NICKNAMEINUSE(char *msg, char **params, struct Server *server, time_t timestamp); +void handle_NICK(char *msg, char **params, struct Server *server, time_t timestamp); + +/* ui.c */ +void ui_init(void); +void ui_read(void); +void ui_redraw(void); +void ui_draw_input(void); +void ui_error_(char *file, int line, char *format, ...); +#define ui_error(format, ...) ui_error_(__FILE__, __LINE__, format, __VA_ARGS__); +void ui_perror_(char *file, int line, char *str); +#define ui_perror(str) ui_perror_(__FILE__, __LINE__, str); +#ifdef TLS +#include <tls.h> +void ui_tls_config_error_(char *file, int line, struct tls_config *config, char *str); +#define ui_tls_config_error(config, str) ui_tls_config_error_(__FILE__, __LINE__, config, str); +void ui_tls_error_(char *file, int line, struct tls *ctx, char *str); +#define ui_tls_error(ctx, str) ui_tls_error_(__FILE__, __LINE__, ctx, str); +#endif /* TLS */ + +/* main.c */ +extern struct HistInfo *main_buf; + +/* ui.c */ +extern struct Channel *selected_channel; +extern struct Server *selected_server; +extern struct Window mainwindow; +extern struct Window inputwindow; +extern struct Window nicklist; +extern struct Window winlist; + +#endif /* H_HIRC */ diff --git a/hist.c b/hist.c @@ -0,0 +1,196 @@ +#include <stdio.h> +#include <fcntl.h> +#include <errno.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <stdarg.h> +#include <ncurses.h> +#include <sys/stat.h> +#include "hirc.h" + +void +hist_free(struct History *history) { + param_free(history->params); + if (history->from) { + free(history->from->prefix); + free(history->from); + } + free(history->raw); + free(history); +} + +void +hist_free_list(struct HistInfo *histinfo) { + struct History *p; + + if (!histinfo->history) + return; + + for (p = histinfo->history->next; p; p = p->next) + hist_free(p->prev); + histinfo->history = NULL; +} + +struct History * +hist_create(struct Server *server, struct Nick *from, char *msg, + char **params, enum Activity activity, + time_t timestamp, enum HistOpt options) { + struct History *new; + + new = emalloc(sizeof(struct History)); + new->prev = new->next = NULL; + new->timestamp = timestamp ? timestamp : time(NULL); + new->activity = activity; + new->raw = estrdup(msg); + new->params = params; + new->options = options; + new->origin = server; + + if (from) { + new->from = nick_dup(from, server); + } else if (**params == ':') { + new->from = nick_create(*params, ' ', server); + } else { + new->from = NULL; + } + + return new; +} + +struct History * +hist_add(struct Server *server, struct HistInfo *histinfo, struct Nick *from, + char *msg, char **params, enum Activity activity, + time_t timestamp, enum HistOpt options) { + struct History *new, *p; + int i; + + if (options & HIST_MAIN || activity == Activity_error) { + if (histinfo->history != main_buf->history) + hist_add(server, main_buf, from, msg, params, activity, timestamp, HIST_SHOW); + else if (options & HIST_MAIN) + ui_error("HIST_MAIN specified, but history is &main_buf", NULL); + } + + new = hist_create(server, from, msg, params, activity, timestamp, options); + + if (histinfo && options & HIST_SHOW && activity > histinfo->activity) + histinfo->activity = activity; + if (histinfo && options & HIST_SHOW && !chan_selected(histinfo->channel) && !serv_selected(histinfo->server)) + histinfo->unread++; + + if (!histinfo->history) { + histinfo->history = new; + return new; + } + + for (i=0, p = histinfo->history; p && p->next; p = p->next, i++); + if (i == (MAX_HISTORY-1)) { + free(p->next); + p->next = NULL; + } + + new->next = histinfo->history; + new->next->prev = new; + histinfo->history = new; + + + // XXX + if (options & HIST_SHOW) { + wprintw(mainwindow.window, "!%lld :%s %s\n", (long long)timestamp, nick_strprefix(from), msg); + refresh(); + } + + if (options & HIST_LOG) { + if (server) + hist_log(new->raw, new->from, new->timestamp, server); + else + ui_error("HIST_LOG specified, but server is NULL", NULL); + } + + return new; +} + +struct History * +hist_format(struct Server *server, struct HistInfo *histinfo, enum Activity activity, enum HistOpt options, char *format, ...) { + char msg[1024], **params; + va_list ap; + + va_start(ap, format); + vsnprintf(msg, sizeof(msg), format, ap); + va_end(ap); + + params = param_create(msg); + + return hist_add(server, histinfo, NULL, msg, params, Activity_status, 0, options); +} + +int +hist_log(char *msg, struct Nick *from, time_t timestamp, struct Server *server) { + char filename[2048]; + int ret, serrno; + + if (*msg == ':' && strchr(msg, ' ')) + msg = strchr(msg, ' ') + 1; + + if (dprintf(server->logfd, "!%lld :%s %s\n", (long long)timestamp, nick_strprefix(from), msg) < 0) { + /* Can't write, try to open the file */ + snprintf(filename, sizeof(filename), "%s/%s.log", homepath(logdir), server->name); + ret = open(filename, O_CREAT|O_APPEND|O_WRONLY); + serrno = errno; + if (ret == -1 && serrno == ENOENT) { + /* No such directory: attempt to create logdir */ + if (mkdir(homepath(logdir), 0700) == -1) { + ui_error("Could not create '%s' directory for logging: %s", logdir, strerror(errno)); + return -1; + } + } else if (ret == -1) { + ui_error("Could not open '%s' for logging: %s", filename, strerror(serrno)); + return -1; + } else { + server->logfd = ret; + } + } else return 1; + + /* retry */ + if (dprintf(server->logfd, "!%lld :%s %s\n", (long long)timestamp, nick_strprefix(from), msg) < 0) { + ui_error("Failed to write to log of server '%s': %s", server->name, strerror(errno)); + return -1; + } + + return 1; +} + +int +hist_len(struct History **history) { + struct History *p; + int i; + + for (i=0, p = *history; p; p = p->next) + i++; + return i; +} + +void +hist_print(struct HistInfo *histinfo, char *prefix) { /* debug */ + struct History *p; + + for (p = histinfo->history; p; p = p->next) + printf("%s: %s\n", prefix, p->raw); +} + +void +hist_print_server(struct Server *server) { /* debug */ + struct Channel *channel; + char prefix[1024]; + + hist_print(server->history, server->name); + for (channel = server->channels; channel; channel = channel->next) { + snprintf(prefix, sizeof(prefix), "%s on %s", channel->name, server->name); + hist_print(channel->history, prefix); + } + for (channel = server->privs; channel; channel = channel->next) { + snprintf(prefix, sizeof(prefix), "query with %s on %s", channel->name, server->name); + hist_print(channel->history, prefix); + } +} diff --git a/main.c b/main.c @@ -0,0 +1,239 @@ +#include <stdio.h> +#include <errno.h> +#include <stdlib.h> +#include <limits.h> +#include <string.h> +#include <unistd.h> +#include <stdarg.h> +#include <signal.h> +#include <poll.h> +#include "hirc.h" + +struct Server *servers = NULL; +struct HistInfo *main_buf; + +void * +emalloc(size_t size) { + void *mem; + + if ((mem = malloc(size)) == NULL) { + perror("malloc()"); + exit(EXIT_FAILURE); + } + + return mem; +} + +char * +estrdup(const char *str) { + char *ret; + + if ((ret = strdup(str)) == NULL) { + perror("strdup()"); + exit(EXIT_FAILURE); + } + + return ret; +} + +void +param_free(char **params) { + char **p; + + for (p = params; p && *p; p++) + free(*p); + free(params); +} + +int +param_len(char **params) { + int i; + + for (i=0; params && *params; i++, params++); + return i; +} + +char ** +param_create(char *msg) { + char **ret, **rp; + char *params[PARAM_MAX]; + char tmp[NL_TEXTMAX]; + char *p, *cur; + int final = 0, i; + + for (i=0; i < PARAM_MAX; i++) + params[i] = NULL; + strlcpy(tmp, msg, sizeof(tmp)); + + for (p=cur=tmp, i=0; p && *p && i < PARAM_MAX; p++) { + if (!final && *p == ':' && *(p-1) == ' ') { + final = 1; + *(p-1) = '\0'; + params[i++] = cur; + cur = p + 1; + } + if (!final && *p == ' ' && *(p+1) != ':') { + *p = '\0'; + params[i++] = cur; + cur = p + 1; + } + } + *p = '\0'; + params[i] = cur; + + ret = emalloc(sizeof(params)); + for (rp=ret, i=0; params[i] && *params[i]; i++, rp++) + *rp = estrdup(params[i]); + *rp = NULL; + + return ret; +} + +int +read_line(int fd, char *buf, size_t buf_len) { + size_t i = 0; + char c = 0; + + do { + if (read(fd, &c, sizeof(char)) != sizeof(char)) + return 0; + if (c != '\r') + buf[i++] = c; + } while (c != '\n' && i < buf_len); + buf[i - 1] = '\0'; + return 1; +} + +int +ircprintf(struct Server *server, char *format, ...) { + char msg[512]; + va_list ap; + int ret, serrno; + + if (server->status == ConnStatus_notconnected) { + ui_error("Not connected to server '%s'", server->name); + return -1; + } + + va_start(ap, format); + if (vsnprintf(msg, sizeof(msg), format, ap) < 0) { + va_end(ap); + return -1; + } + + ret = write(server->wfd, msg, strlen(msg)); + + if (ret == -1 && server->status == ConnStatus_connected) { + serv_disconnect(server, 1); + hist_format(server, server->history, Activity_error, HIST_SHOW, + "SELF_CONNECTLOST %s %s %s :%s", + server->name, server->host, server->port, strerror(errno)); + } else if (ret == -1 && server->status != ConnStatus_connecting) { + ui_error("Not connected to server '%s'", server->name); + } + + va_end(ap); + return ret; +} + +char * +homepath(char *path) { + static char ret[1024]; + + if (*path == '~') { + snprintf(ret, sizeof(ret), "%s/%s", getenv("HOME"), path + 1); + return ret; + } + + return path; +} + +char +chrcmp(char c, char *s) { + for (; s && *s; s++) + if (c == *s) + return c; + + return 0; +} + +char * +struntil(char *str, char until) { + static char ret[1024]; + int i; + + for (i=0; str && *str && i < 1024 && *str != until; i++, str++) + ret[i] = *str; + + ret[i] = '\0'; + return ret; +} + +void +sighandler(int signal) { + return; +} + +int +main(int argc, char **argv) { + struct Server *sp; + FILE *file; + struct pollfd fds[] = { + { .fd = fileno(stdin), .events = POLLIN }, + }; + + main_buf = emalloc(sizeof(struct HistInfo)); + main_buf->activity = Activity_ignore; + main_buf->unread = 0; + main_buf->server = NULL; + main_buf->channel = NULL; + main_buf->history = NULL; + + ui_init(); + serv_add(&servers, "hlircnet", "irc.hhvn.uk", "6667", "hhvn", "Fanatic", "gopher://hhvn.uk", 1, 0); + /* serv_add(&servers, "dataswamp", "127.0.0.1", "6697", "hhvn", "Fanatic", "gopher://hhvn.uk", 1, 0); */ + for (sp = servers; sp; sp = sp->next) + serv_connect(sp); + + for (;;) { + if (serv_poll(&servers, 5) < 0) { + perror("serv_poll()"); + exit(EXIT_FAILURE); + } + + for (sp = servers; sp; sp = sp->next) { + if (sp->rpollfd->revents) { + /* received an event */ + sp->pingsent = 0; + sp->lastrecv = time(NULL); + sp->rpollfd->revents = 0; + handle(sp->rfd, sp); + } else if (!sp->pingsent && sp->lastrecv && (time(NULL) - sp->lastrecv) >= pinginact) { + /* haven't heard from server in pinginact seconds, sending a ping */ + ircprintf(sp, "PING :ground control to Major Tom\r\n"); + sp->pingsent = time(NULL); + } else if (sp->pingsent && (time(NULL) - sp->pingsent) >= pinginact) { + /* haven't gotten a response in pinginact seconds since + * sending ping, this connexion is probably dead now */ + serv_disconnect(sp, 1); + hist_format(sp, sp->history, Activity_error, HIST_SHOW, + "SELF_CONNECTLOST %s %s %s :No ping reply in %d seconds", + sp->name, sp->host, sp->port, pinginact); + } else if (sp->status == ConnStatus_notconnected && sp->reconnect && + (time(NULL) - sp->lastconnected) >= (sp->connectfail * reconnectinterval)) { + /* time since last connected is sufficient to initiate reconnect */ + serv_connect(sp); + } + } + + ui_read(); + wrefresh(nicklist.window); + wrefresh(winlist.window); + wrefresh(mainwindow.window); + + /* always refresh input last */ + wrefresh(inputwindow.window); + } + + return 0; +} diff --git a/nick.c b/nick.c @@ -0,0 +1,200 @@ +#include <stdio.h> +#include <ctype.h> +#include <stdlib.h> +#include <string.h> +#include "hirc.h" + +#define MAX(var1, var2) (var1 > var2 ? var1 : var2) +#define MIN(var1, var2) (var1 < var2 ? var1 : var2) +#define MAXA(array) MAX(array[0], array[1]) +#define MINA(array) MIN(array[0], array[1]) + +unsigned short +nick_getcolour(char *nick) { + unsigned short ret, sum; + int i; + + if (othercolour[0] == othercolour[1]) + return othercolour[0]; + + for (sum=i=0; nick && *nick; nick++, i++) { + /* don't count certain trailing characters. The following: + * hhvn + * hhvn_ + * hhvn2 + * should all produce the same colour. */ + if ((*nick == '_' || isdigit(*nick)) && *(nick + 1) == '\0') + break; + + sum += *nick * (i + 1); + sum ^= *nick; + } + + return (sum % (MAXA(othercolour) - MINA(othercolour)) + MINA(othercolour) - 1); +} + +void +prefix_tokenize(char *prefix, char **nick, char **ident, char **host) { + enum { ISNICK, ISIDENT, ISHOST } segment = ISNICK; + + if (*prefix == ':') + prefix++; + + if (nick) *nick = prefix; + if (ident) *ident = NULL; + if (host) *host = NULL; + + for (; prefix && *prefix && segment != ISHOST; prefix++) { + if (segment == ISNICK && *prefix == '!') { + *prefix = '\0'; + if (ident) + *ident = prefix + 1; + segment = ISIDENT; + } + if (segment == ISIDENT && *prefix == '@') { + *prefix = '\0'; + if (host) + *host = prefix + 1; + segment = ISHOST; + } + } +} + +void +nick_free(struct Nick *nick) { + if (nick) { + free(nick->prefix); + free(nick); + } +} + +void +nick_free_list(struct Nick **head) { + struct Nick *p; + + if (!head || !*head) + return; + + for (p = (*head)->next; p; p = p->next) + nick_free(p->prev); + *head = NULL; +} + +struct Nick * +nick_create(char *prefix, char priv, struct Server *server) { + struct Nick *nick; + + if (!prefix || !priv) + return NULL; + + nick = emalloc(sizeof(struct Nick)); + nick->prefix = estrdup(prefix); + nick->next = nick->prev = NULL; + nick->priv = priv; + prefix_tokenize(nick->prefix, &nick->nick, &nick->ident, &nick->host); + nick->self = nick_isself_server(nick, server); + + return nick; +} + +int +nick_isself(struct Nick *nick) { + if (!nick) + return 0; + + return nick->self; +} + +int +nick_isself_server(struct Nick *nick, struct Server *server) { + if (!nick || !server || !nick->nick) + return 0; + + if (strcmp(server->self->nick, nick->nick) == 0) + return 1; + else + return 0; +} + +struct Nick * +nick_add(struct Nick **head, char *prefix, char priv, struct Server *server) { + struct Nick *nick, *p; + + if (!prefix || !priv) + return NULL; + + if ((nick = nick_create(prefix, priv, server)) == NULL) + return NULL; + + if (!*head) { + *head = nick; + return nick; + } + + p = *head; + for (; p && p->next; p = p->next); + p->next = nick; + nick->prev = p; + + return nick; +} + +struct Nick * +nick_dup(struct Nick *nick, struct Server *server) { + return nick_create(nick->prefix, nick->priv, server); +} + +struct Nick * +nick_get(struct Nick **head, char *nick) { + struct Nick *p; + + p = *head; + for (; p; p = p->next) { + if (strcmp(p->nick, nick) == 0) + return p; + } + + return NULL; +} + +int +nick_remove(struct Nick **head, char *nick) { + struct Nick *p; + + if (!head || !nick) + return -1; + + if ((p = nick_get(head, nick)) == NULL) + return 0; + + if (p->prev == NULL) { + *head = p->next; + nick_free(p); + return 1; + } + + p->prev->next = p->next; + if (p->next != NULL) + p->next->prev = p->prev; + nick_free(p); + return 1; +} + +char * +nick_strprefix(struct Nick *nick) { + static char ret[1024]; + + if (!nick) + return NULL; + + if (nick->nick && nick->ident && nick->host) + snprintf(ret, sizeof(ret), "%s!%s@%s", nick->nick, nick->ident, nick->host); + else if (nick->nick && nick->ident) + snprintf(ret, sizeof(ret), "%s!%s", nick->nick, nick->ident); + else if (nick->nick) + snprintf(ret, sizeof(ret), "%s", nick->nick); + else + snprintf(ret, sizeof(ret), ""); + + return ret; +} diff --git a/serv.c b/serv.c @@ -0,0 +1,328 @@ +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <netdb.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <poll.h> +#ifdef TLS +#include <tls.h> +#endif /* TLS */ +#include "hirc.h" + +void +serv_free(struct Server *server) { + struct Support *p; + + if (!server) + return; + + free(server->name); + free(server->username); + free(server->realname); + free(server->host); + free(server->port); + free(server->rpollfd); + nick_free(server->self); + hist_free_list(server->history); + chan_free_list(&server->channels); + chan_free_list(&server->privs); + for (p = server->supports; p; p = p->next) { + free(p->prev); + free(p->key); + free(p->value); + } +#ifdef TLS + if (server->tls) + tls_free(server->tls_ctx); +#endif /* TLS */ + free(p); +} + +struct Server * +serv_create(char *name, char *host, char *port, char *nick, + char *username, char *realname, int tls, int tls_verify) { + struct Server *server; + struct tls_config *conf; + + if (!name || !host || !port || !nick) + return NULL; + + server = emalloc(sizeof(struct Server)); + server->prev = server->next = NULL; + server->wfd = server->rfd = server->logfd = -1; + server->rpollfd = emalloc(sizeof(struct pollfd)); + server->rpollfd->fd = -1; + server->rpollfd->events = POLLIN; + server->rpollfd->revents = 0; + server->status = ConnStatus_notconnected; + server->name = estrdup(name); + server->username = username ? estrdup(username) : NULL; + server->realname = realname ? estrdup(realname) : NULL; + server->host = estrdup(host); + server->port = estrdup(port); + server->supports = NULL; + support_set(server, "CHANTYPES", default_chantypes); + support_set(server, "PREFIX", default_prefixes); + server->self = nick_create(nick, ' ', NULL); + server->self->self = 1; + server->history = emalloc(sizeof(struct HistInfo)); + server->history->activity = Activity_ignore; + server->history->unread = 0; + server->history->server = server; + server->history->channel = NULL; + server->history->history = NULL; + server->channels = NULL; + server->privs = NULL; + server->reconnect = 0; + server->connectfail = 0; + server->lastconnected = server->lastrecv = server->pingsent = 0; + +#ifdef TLS + server->tls = tls; + server->tls_ctx = NULL; + if (server->tls && (conf = tls_config_new()) == NULL) { + ui_tls_config_error(conf, "tls_config_new()"); + server->tls = 0; + } + + if (server->tls && !tls_verify) { + tls_config_insecure_noverifycert(conf); + tls_config_insecure_noverifyname(conf); + } + + if (server->tls && (server->tls_ctx = tls_client()) == NULL) { + ui_perror("tls_client()"); + server->tls = 0; + } + + if (server->tls && tls_configure(server->tls_ctx, conf) == -1) { + ui_tls_error(server->tls_ctx, "tls_configure()"); + server->tls = 0; + } + + tls_config_free(conf); +#else + if (tls) + hist_format(server, server->history, Activity_error, HIST_SHOW, + "SELF_TLSNOTCOMPILED %s", server->name); +#endif /* TLS */ + + return server; +} + +struct Server * +serv_add(struct Server **head, char *name, char *host, char *port, + char *nick, char *username, char *realname, int tls, int tls_verify) { + struct Server *new, *p; + + if ((new = serv_create(name, host, port, nick, username, realname, tls, tls_verify)) == NULL) + return NULL; + + if (!*head) { + *head = new; + return new; + } + + p = *head; + for (; p && p->next; p = p->next); + p->next = new; + new->prev = p; + + return new; +} + +struct Server * +serv_get(struct Server **head, char *name) { + struct Server *p; + + if (!head || !*head || !name) + return NULL; + + for (p = *head; p; p = p->next) { + if (strcmp(p->name, name) == 0) + return p; + } + + return NULL; +} + +struct Server * +serv_get_byrfd(struct Server **head, int rfd) { + struct Server *p; + + if (!head || !*head) + return NULL; + + for (p = *head; p; p = p->next) { + if (p->rfd == rfd) + return p; + } + + return NULL; +} + +int +serv_remove(struct Server **head, char *name) { + struct Server *p; + + if (!head || !name) + return -1; + + if ((p = serv_get(head, name)) == NULL) + return 0; + + if (p->prev = NULL) { + *head = p->next; + serv_free(p); + return 1; + } + + p->prev->next = p->next; + if (p->next != NULL) + p->next->prev = p->prev; + serv_free(p); + return 1; +} + +void +serv_connect(struct Server *server) { + struct addrinfo hints; + struct addrinfo *ai; + int fd, ret, serrno; + + server->status = ConnStatus_connecting; + hist_format(server, server->history, Activity_status, HIST_SHOW|HIST_MAIN, + "SELF_CONNECTING %s %s", server->host, server->port); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + if ((ret = getaddrinfo(server->host, server->port, &hints, &ai)) != 0 || ai == NULL) { + hist_format(server, server->history, Activity_error, HIST_SHOW, + "SELF_LOOKUPFAIL %s %s %s :%s", + server->name, server->host, server->port, gai_strerror(ret)); + goto fail; + } + if ((fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol)) == -1 || connect(fd, ai->ai_addr, ai->ai_addrlen) == -1) { + hist_format(server, server->history, Activity_error, HIST_SHOW, + "SELF_CONNECTFAIL %s %s %s :%s", + server->name, server->host, server->port, strerror(errno)); + goto fail; + } + + server->connectfail = 0; + server->status = ConnStatus_connected; + server->rfd = server->wfd = fd; + hist_format(server, server->history, Activity_status, HIST_SHOW|HIST_MAIN, + "SELF_CONNECTED %s %s %s", server->name, server->host, server->port); + freeaddrinfo(ai); + + ircprintf(server, "NICK %s\r\n", server->self->nick); + ircprintf(server, "USER %s * * :%s\r\n", + server->username ? server->username : server->self->nick, + server->realname ? server->realname : server->self->nick); + + return; + +fail: + serv_disconnect(server, 1); + if (server->connectfail * reconnectinterval < maxreconnectinterval) + server->connectfail += 1; + freeaddrinfo(ai); +} + +int +serv_len(struct Server **head) { + struct Server *p; + int i; + + for (p = *head, i = 0; p; p = p->next) + i++; + return i; +} + +int +serv_poll(struct Server **head, int timeout) { + struct pollfd fds[64]; + struct Server *sp; + int i, ret; + + for (i=0, sp = *head; sp; sp = sp->next, i++) { + sp->rpollfd->fd = sp->rfd; + fds[i].fd = sp->rpollfd->fd; + fds[i].events = POLLIN; + } + + ret = poll(fds, serv_len(head), timeout); + if (errno == EINTR) /* ncurses issue */ + ret = 0; + + for (i=0, sp = *head; sp; sp = sp->next, i++) + if (sp->status == ConnStatus_connecting + || sp->status == ConnStatus_connected) + sp->rpollfd->revents = fds[i].revents; + + return ret; +} + +void +serv_disconnect(struct Server *server, int reconnect) { + shutdown(server->rfd, SHUT_RDWR); + shutdown(server->wfd, SHUT_RDWR); + close(server->rfd); + close(server->wfd); + + server->rfd = server->wfd = server->rpollfd->fd = -1; + server->status = ConnStatus_notconnected; + server->lastrecv = server->pingsent = 0; + server->lastconnected = time(NULL); + server->reconnect = reconnect; +} + +int +serv_selected(struct Server *server) { + if (!selected_channel && selected_server == server) + return 1; + else + return 0; +} + +char * +support_get(struct Server *server, char *key) { + struct Support *p; + for (p = server->supports; p; p = p->next) + if (strcmp(p->key, key) == 0) + return p->value; + + return NULL; +} + +void +support_set(struct Server *server, char *key, char *value) { + struct Support *p; + + if (!server->supports) { + server->supports = malloc(sizeof(struct Support)); + server->supports->prev = server->supports->next = NULL; + server->supports->key = key ? strdup(key) : NULL; + server->supports->value = value ? strdup(value) : NULL; + return; + } + + for (p = server->supports; p && p->next; p = p->next) { + if (strcmp(p->key, key) == 0) { + free(p->value); + p->value = strdup(value); + return; + } + } + + p->next = malloc(sizeof(struct Support)); + p->next->prev = p; + p->next->next = NULL; + p->next->key = key ? strdup(key) : NULL; + p->next->value = value ? strdup(value) : NULL; +} diff --git a/struct.h b/struct.h @@ -0,0 +1,144 @@ +#ifndef H_STRUCT +#define H_STRUCT + +#include <time.h> +#include <sys/time.h> +#include <poll.h> + +struct Nick { + struct Nick *prev; + char priv; /* [~&@%+ ] */ + char *prefix; /* don't read from this, nick, ident, host are + pointers to segments of this. + printf(":%s!%s@%s\n", nick, ident, host); */ + char *nick; + char *ident; + char *host; + int self; + struct Nick *next; +}; + +enum Activity { + Activity_ignore, + Activity_status, + Activity_notice = Activity_status, + Activity_error, + Activity_message, + Activity_hilight, +}; + +enum HistOpt { + HIST_SHOW = 1, /* show in buffer */ + HIST_LOG = 2, /* log to server->logfd */ + HIST_MAIN = 4, /* copy to &main_buf */ + HIST_DFL = HIST_SHOW|HIST_LOG +}; + +struct History { + struct History *prev; + time_t timestamp; + enum Activity activity; + enum HistOpt options; + char *raw; + char **params; + struct Server *origin; + struct Nick *from; + struct History *next; +}; + +struct HistInfo { + enum Activity activity; + int unread; + struct Server *server; + struct Channel *channel; + struct History *history; +}; + +struct Channel { + struct Channel *prev; + int old; /* are we actually in this channel, + or just keeping it for memory */ + char *name; + char *mode; + char *topic; + struct Nick *nicks; + struct HistInfo *history; + struct Server *server; + struct Channel *next; +}; + +enum ConnStatus { + ConnStatus_notconnected, + ConnStatus_connecting, + ConnStatus_connected, + ConnStatus_file, +}; + +struct Support { + struct Support *prev; + char *key; + char *value; + struct Support *next; +}; + +struct Server { + struct Server *prev; + int wfd; + int rfd; + struct pollfd *rpollfd; + int logfd; + enum ConnStatus status; + char *name; + char *username; + char *realname; + char *host; + char *port; + struct Support *supports; + struct Nick *self; + struct HistInfo *history; + struct Channel *channels; + struct Channel *privs; + int reconnect; + int connectfail; /* number of failed connections */ + time_t lastconnected; /* last time a connection was lost */ + time_t lastrecv; /* last time a message was received from server */ + time_t pingsent; /* last time a ping was sent to server */ +#ifdef TLS + int tls; + struct tls *tls_ctx; +#endif /* TLS */ + struct Server *next; +}; + +struct Handler { + char *cmd; /* or numeric */ + void (*func)(char *msg, char **params, struct Server *server, time_t timestamp); +}; + +struct Netconfig { + char *name; + char *host; + char *port; + char *nick; + char *user; + char *real; + char *join[64]; + int tls; + int tls_verify; +}; + +enum WindowLocation { + HIDDEN, + LEFT, + RIGHT, +}; + +#include <ncurses.h> +struct Window { + int x, y; + int h, w; + enum WindowLocation location; + WINDOW *window; +}; + +#endif /* H_STRUCT */ diff --git a/ui.c b/ui.c @@ -0,0 +1,240 @@ +#include <errno.h> +#include <ctype.h> +#include <stdarg.h> +#include <string.h> +#include <stdlib.h> +#include <ncurses.h> +#ifdef TLS +#include <tls.h> +#endif /* TLS */ +#include "hirc.h" + +struct HistInfo *error_buf; +struct Window mainwindow; +struct Window inputwindow; +struct Window nicklist; +struct Window winlist; + +struct Channel *selected_channel = NULL; +struct Server *selected_server = NULL; + +struct { + char string[8192]; + unsigned counter; +} input; + +void +ui_error_(char *file, int line, char *format, ...) { + char msg[1024]; + va_list ap; + + va_start(ap, format); + vsnprintf(msg, sizeof(msg), format, ap); + va_end(ap); + + hist_format(NULL, error_buf, Activity_error, HIST_SHOW, + "SELF_ERROR %s %d :%s", + file, line, msg); +} + +void +ui_perror_(char *file, int line, char *str) { + hist_format(NULL, error_buf, Activity_error, HIST_SHOW, + "SELF_ERROR %s %d :%s: %s", + file, line, str, strerror(errno)); +} + +#ifdef TLS +void +ui_tls_config_error_(char *file, int line, struct tls_config *config, char *str) { + hist_format(NULL, error_buf, Activity_error, HIST_SHOW, + "SELF_ERROR %s %d :%s: %s", + file, line, str, tls_config_error(config)); +} + +void +ui_tls_error_(char *file, int line, struct tls *ctx, char *str) { + hist_format(NULL, error_buf, Activity_error, HIST_SHOW, + "SELF_ERROR %s %d :%s: %s", + file, line, str, tls_error(ctx)); +} +#endif /* TLS */ + +void +ui_init(void) { + initscr(); + raw(); + noecho(); + nodelay(stdscr, TRUE); + keypad(stdscr, TRUE); + + memset(input.string, '\0', sizeof(input.string)); + input.counter = 0; + + error_buf = emalloc(sizeof(struct HistInfo)); + error_buf->activity = Activity_ignore; + error_buf->unread = 0; + error_buf->server = NULL; + error_buf->channel = NULL; + error_buf->history = NULL; + + if (nicklistlocation != 0 && nicklistlocation == winlistlocation) { + ui_error("nicklist and winlist can't be set to same location in config.h", NULL); + winlist.location = LEFT; + nicklist.location = RIGHT; + } else { + winlist.location = winlistlocation; + nicklist.location = nicklistlocation; + } + + mainwindow.window = newwin(0, 0, 0, 0); + inputwindow.window = newwin(0, 0, 0, 0); + mainwindow.location = -1; + inputwindow.location = -1; + if (nicklist.location) + nicklist.window = newwin(0, 0, 0, 0); + if (winlist.location) + winlist.window = newwin(0, 0, 0, 0); + + ui_redraw(); + + wprintw(nicklist.window, "nicklist"); + wprintw(winlist.window, "winlist"); +} + +void +ui_placewindow(struct Window *window) { + if (window->location != HIDDEN) { + wresize(window->window, window->h, window->w); + mvwin(window->window, window->y, window->x); + wrefresh(window->window); + } +} + +void +ui_read(void) { + int key; + + switch (key = wgetch(stdscr)) { + case KEY_RESIZE: + ui_redraw(); + break; + case KEY_BACKSPACE: + if (input.counter) { + memmove(&input.string[input.counter - 1], + &input.string[input.counter], + strlen(&input.string[input.counter]) + 1); + input.counter--; + ui_draw_input(); + } + break; + case KEY_LEFT: + if (input.counter) { + input.counter--; + ui_draw_input(); + } + break; + case KEY_RIGHT: + if (input.string[input.counter]) { + input.counter++; + ui_draw_input(); + } + break; + case '\n': + if (strcmp(input.string, "/quit") == 0) { + endwin(); + exit(0); + } + wprintw(mainwindow.window, "%s\n", input.string); + memset(input.string, '\0', sizeof(input.string)); + input.counter = 0; + ui_draw_input(); + break; + default: + if (isprint(key) || iscntrl(key)) { + memmove(&input.string[input.counter + 1], + &input.string[input.counter], + strlen(&input.string[input.counter])); + input.string[input.counter++] = key; + ui_draw_input(); + } + } +} + +void +ui_redraw(void) { + int x = 0, rx = 0; + + if (winlist.location == LEFT) { + winlist.x = winlist.y = 0; + winlist.h = LINES; + winlist.w = winlistwidth; + x = winlist.w + 1; + } + if (nicklist.location == LEFT) { + nicklist.x = winlist.y = 0; + nicklist.h = LINES; + nicklist.w = nicklistwidth; + x = nicklist.w + 1; + } + if (winlist.location == RIGHT) { + winlist.x = COLS - winlistwidth; + winlist.y = 0; + winlist.h = LINES; + winlist.w = winlistwidth; + rx = winlistwidth + 1; + } + if (nicklist.location == RIGHT) { + nicklist.x = COLS - nicklistwidth; + nicklist.y = 0; + nicklist.h = LINES; + nicklist.w = nicklistwidth; + rx = nicklistwidth + 1; + } + + mainwindow.x = x; + mainwindow.y = 0; + mainwindow.h = LINES - 2; + mainwindow.w = COLS - x - rx; + + inputwindow.x = x; + inputwindow.y = LINES - 1; + inputwindow.h = 1; + inputwindow.w = COLS - x - rx; + + ui_placewindow(&nicklist); + ui_placewindow(&winlist); + ui_placewindow(&mainwindow); + ui_placewindow(&inputwindow); + + if (x) + mvvline(0, x - 1, '|', LINES); + if (rx) + mvvline(0, COLS - rx, '|', LINES); + + mvhline(LINES - 2, x, '-', COLS - x - rx); + + ui_draw_input(); +} + +void +ui_draw_input(void) { + char *p; + char tmp; + int offset; + + wclear(inputwindow.window); + /* Round input.counter down to the nearest inputwindow.w. + * This gives "pages" that are each as long as the width of the input window */ + offset = ((int) input.counter / inputwindow.w) * inputwindow.w; + for (p = input.string + offset; p && *p; p++) { + if (iscntrl(*p)) { + /* adding 64 will turn ^C into C */ + tmp = *p + 64; + wattron(inputwindow.window, A_REVERSE); + waddnstr(inputwindow.window, &tmp, 1); + wattroff(inputwindow.window, A_REVERSE); + } else waddnstr(inputwindow.window, p, 1); + } + wmove(inputwindow.window, 0, input.counter - offset); +}