|
|
|
@@ -151,6 +151,7 @@ typedef struct {
|
|
|
|
|
int mention;
|
|
|
|
|
int scroll;
|
|
|
|
|
char topic[MAX_LINE];
|
|
|
|
|
int names_pending; /* 1 while receiving fresh 353 NAMES reply */
|
|
|
|
|
} Channel;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
@@ -162,7 +163,7 @@ typedef enum {
|
|
|
|
|
EV_CONNECTED, EV_STATUS, EV_ERROR, EV_SERVER_TEXT,
|
|
|
|
|
EV_PRIVMSG, EV_JOIN, EV_PART, EV_QUIT_MSG,
|
|
|
|
|
EV_NICK_CHANGE, EV_NAMES, EV_KICK, EV_RAW,
|
|
|
|
|
EV_RECONNECT, EV_TOPIC
|
|
|
|
|
EV_RECONNECT, EV_TOPIC, EV_NAMES_END
|
|
|
|
|
} EvType;
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
@@ -187,15 +188,16 @@ typedef struct {
|
|
|
|
|
char sasl_pass[256];
|
|
|
|
|
|
|
|
|
|
/* runtime */
|
|
|
|
|
int connected;
|
|
|
|
|
volatile int connected;
|
|
|
|
|
volatile int net_stop;
|
|
|
|
|
pthread_t net_tid;
|
|
|
|
|
pthread_mutex_t send_lock;
|
|
|
|
|
int sock;
|
|
|
|
|
SSL *ssl;
|
|
|
|
|
|
|
|
|
|
int reconnect_pending;
|
|
|
|
|
volatile int reconnect_pending;
|
|
|
|
|
pthread_t reconnect_tid;
|
|
|
|
|
int sasl_auth_sent; /* guard: only send AUTHENTICATE once per session */
|
|
|
|
|
|
|
|
|
|
/* per-server event pipe */
|
|
|
|
|
int evpipe[2];
|
|
|
|
@@ -206,11 +208,12 @@ typedef struct {
|
|
|
|
|
static Server g_srv[MAX_SERVERS];
|
|
|
|
|
static int g_srv_count = 0;
|
|
|
|
|
static SSL_CTX *g_ssl_ctx = NULL;
|
|
|
|
|
static pthread_mutex_t g_ssl_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
|
|
|
|
|
|
/* channels (flat array, each has a .srv index) */
|
|
|
|
|
static Channel g_chans[MAX_CHANS_TOTAL];
|
|
|
|
|
static int g_chan_count = 0;
|
|
|
|
|
static int g_active = 0; /* index into g_chans; -1 = no channels */
|
|
|
|
|
static int g_active = -1; /* index into g_chans; -1 = no channels */
|
|
|
|
|
|
|
|
|
|
/* global ignore list */
|
|
|
|
|
static char g_ignore[MAX_IGNORE][MAX_NICK];
|
|
|
|
@@ -252,6 +255,7 @@ static void str_upper(char *dst, const char *src, int n) {
|
|
|
|
|
|
|
|
|
|
static int str_icase_starts(const char *hay, const char *needle) {
|
|
|
|
|
while (*needle) {
|
|
|
|
|
if (!*hay) return 0;
|
|
|
|
|
if (tolower((unsigned char)*hay) != tolower((unsigned char)*needle))
|
|
|
|
|
return 0;
|
|
|
|
|
hay++; needle++;
|
|
|
|
@@ -282,7 +286,7 @@ static int chan_find(int srv, const char *name) {
|
|
|
|
|
static int chan_add(int srv, const char *name) {
|
|
|
|
|
int i = chan_find(srv, name);
|
|
|
|
|
if (i >= 0) return i;
|
|
|
|
|
if (g_chan_count >= MAX_CHANS_TOTAL) return 0;
|
|
|
|
|
if (g_chan_count >= MAX_CHANS_TOTAL) return -1;
|
|
|
|
|
i = g_chan_count++;
|
|
|
|
|
memset(&g_chans[i], 0, sizeof(Channel));
|
|
|
|
|
strncpy(g_chans[i].name, name, MAX_CHAN-1);
|
|
|
|
@@ -343,7 +347,8 @@ static void chan_addline(Channel *ch, int flag, const char *text) {
|
|
|
|
|
|
|
|
|
|
/* get the status channel for a server (create if needed) */
|
|
|
|
|
static int srv_status_chan(int srv) {
|
|
|
|
|
return chan_add(srv, "*status*");
|
|
|
|
|
int ci = chan_add(srv, "*status*");
|
|
|
|
|
return ci >= 0 ? ci : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void srv_status_msg(int srv, const char *msg) {
|
|
|
|
@@ -375,7 +380,7 @@ static int user_find(Channel *ch, const char *nick) {
|
|
|
|
|
|
|
|
|
|
static void chan_adduser(Channel *ch, char mode, const char *nick) {
|
|
|
|
|
int idx = user_find(ch, nick);
|
|
|
|
|
char entry[MAX_NICK];
|
|
|
|
|
char entry[MAX_NICK] = {0};
|
|
|
|
|
entry[0] = mode ? mode : ' ';
|
|
|
|
|
strncpy(entry+1, nick, MAX_NICK-2);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
@@ -486,8 +491,13 @@ static void ignore_remove(const char *nick) {
|
|
|
|
|
/* ── event pipe ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
static void ev_push(const Event *ev) {
|
|
|
|
|
/* non-blocking pipe: retry once on EAGAIN so events aren't silently lost */
|
|
|
|
|
if (write(g_evpipe[1], ev, sizeof(Event)) < 0) {
|
|
|
|
|
struct timespec ts = {0, 10000000}; /* 10 ms */
|
|
|
|
|
nanosleep(&ts, NULL);
|
|
|
|
|
write(g_evpipe[1], ev, sizeof(Event));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ev_simple(EvType type, int srv, const char *nick,
|
|
|
|
|
const char *chan, const char *text) {
|
|
|
|
@@ -519,8 +529,10 @@ static int srv_send_raw(int si, const char *line) {
|
|
|
|
|
static void srv_sendf(int si, const char *fmt, ...) {
|
|
|
|
|
char buf[MAX_LINE];
|
|
|
|
|
va_list ap; va_start(ap, fmt);
|
|
|
|
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
|
|
|
int n = vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
|
|
|
va_end(ap);
|
|
|
|
|
if (n >= (int)sizeof(buf))
|
|
|
|
|
ev_simple(EV_ERROR, si, NULL, NULL, "send truncated (increase MAX_LINE)");
|
|
|
|
|
srv_send_raw(si, buf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -579,15 +591,16 @@ static const char b64tab[] =
|
|
|
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
|
|
|
|
|
|
static void base64_encode(const unsigned char *in, int inlen, char *out) {
|
|
|
|
|
int i=0, j=0;
|
|
|
|
|
while (i < inlen) {
|
|
|
|
|
unsigned int a=i<inlen?(unsigned char)in[i++]:0;
|
|
|
|
|
unsigned int b=i<inlen?(unsigned char)in[i++]:0;
|
|
|
|
|
unsigned int c=i<inlen?(unsigned char)in[i++]:0;
|
|
|
|
|
int j = 0;
|
|
|
|
|
for (int i = 0; i < inlen; i += 3) {
|
|
|
|
|
unsigned int a = (unsigned char)in[i];
|
|
|
|
|
unsigned int b = (i+1 < inlen) ? (unsigned char)in[i+1] : 0;
|
|
|
|
|
unsigned int c = (i+2 < inlen) ? (unsigned char)in[i+2] : 0;
|
|
|
|
|
unsigned int n = (a << 16) | (b << 8) | c;
|
|
|
|
|
out[j++]=b64tab[(n>>18)&63]; out[j++]=b64tab[(n>>12)&63];
|
|
|
|
|
out[j++]=(i-2<=inlen)?b64tab[(n>>6)&63]:'=';
|
|
|
|
|
out[j++]=(i-1<=inlen)?b64tab[n&63]:'=';
|
|
|
|
|
out[j++] = b64tab[(n >> 18) & 63];
|
|
|
|
|
out[j++] = b64tab[(n >> 12) & 63];
|
|
|
|
|
out[j++] = (i+1 < inlen) ? b64tab[(n >> 6) & 63] : '=';
|
|
|
|
|
out[j++] = (i+2 < inlen) ? b64tab[n & 63] : '=';
|
|
|
|
|
}
|
|
|
|
|
out[j] = '\0';
|
|
|
|
|
}
|
|
|
|
@@ -604,25 +617,65 @@ static void handle_irc_line(int si, const char *raw) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* SASL */
|
|
|
|
|
if (strcmp(m.cmd,"PONG")==0) return; /* ignore server's PONG replies */
|
|
|
|
|
|
|
|
|
|
/* SASL / CAP negotiation */
|
|
|
|
|
if (strcmp(m.cmd,"CAP")==0) {
|
|
|
|
|
char sub[16]="";
|
|
|
|
|
if (m.nparams>=2) str_upper(sub, m.params[1], sizeof(sub));
|
|
|
|
|
if (strcmp(sub,"ACK")==0 && strstr(m.trail,"sasl")) {
|
|
|
|
|
else if (m.nparams>=1) str_upper(sub, m.params[0], sizeof(sub));
|
|
|
|
|
|
|
|
|
|
/* log all CAP lines to status for visibility */
|
|
|
|
|
char capdbg[MAX_LINE];
|
|
|
|
|
snprintf(capdbg,sizeof(capdbg),"[CAP] %s",raw);
|
|
|
|
|
ev_simple(EV_STATUS, si, NULL, NULL, capdbg);
|
|
|
|
|
|
|
|
|
|
if (strcmp(sub,"LS")==0) {
|
|
|
|
|
/* server advertising capabilities -- request sasl */
|
|
|
|
|
srv_send_raw(si, "CAP REQ :sasl");
|
|
|
|
|
} else if (strcmp(sub,"ACK")==0) {
|
|
|
|
|
const char *caps = m.trail[0] ? m.trail : (m.nparams>0?m.params[m.nparams-1]:"");
|
|
|
|
|
if (strstr(caps,"sasl")) {
|
|
|
|
|
if (!s->sasl_auth_sent) {
|
|
|
|
|
s->sasl_auth_sent = 1;
|
|
|
|
|
srv_send_raw(si, "AUTHENTICATE PLAIN");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
srv_send_raw(si, "CAP END");
|
|
|
|
|
}
|
|
|
|
|
} else if (strcmp(sub,"NAK")==0) {
|
|
|
|
|
ev_simple(EV_ERROR, si, NULL, NULL, "Server rejected SASL CAP.");
|
|
|
|
|
ev_simple(EV_ERROR, si, NULL, NULL, "Server rejected SASL CAP -- connecting without SASL.");
|
|
|
|
|
srv_send_raw(si, "CAP END");
|
|
|
|
|
srv_sendf(si, "NICK %s", s->nick);
|
|
|
|
|
srv_sendf(si, "USER %s 0 * :sirc", s->nick);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (strcmp(m.cmd,"AUTHENTICATE")==0 && strcmp(m.trail,"+")==0) {
|
|
|
|
|
if (strcmp(m.cmd,"AUTHENTICATE")==0) {
|
|
|
|
|
/* server sends AUTHENTICATE + (trail or param) to prompt us */
|
|
|
|
|
const char *challenge = m.trail[0] ? m.trail :
|
|
|
|
|
(m.nparams>0 ? m.params[0] : "");
|
|
|
|
|
if (strcmp(challenge,"+")==0) {
|
|
|
|
|
/* build PLAIN payload: authzid\0authcid\0passwd
|
|
|
|
|
must use memcpy — sasl_user/pass may not contain \0
|
|
|
|
|
but we need to embed \0 separators manually */
|
|
|
|
|
char payload[512];
|
|
|
|
|
int plen=snprintf(payload,sizeof(payload),"%s%c%s%c%s",
|
|
|
|
|
s->sasl_user,0,s->sasl_user,0,s->sasl_pass);
|
|
|
|
|
char enc[700]; base64_encode((unsigned char*)payload,plen,enc);
|
|
|
|
|
int ulen = strlen(s->sasl_user);
|
|
|
|
|
int plen_pass = strlen(s->sasl_pass);
|
|
|
|
|
/* layout: [sasl_user]\0[sasl_user]\0[sasl_pass] */
|
|
|
|
|
int total = ulen + 1 + ulen + 1 + plen_pass;
|
|
|
|
|
if (total < (int)sizeof(payload)) {
|
|
|
|
|
int pos = 0;
|
|
|
|
|
memcpy(payload + pos, s->sasl_user, ulen); pos += ulen;
|
|
|
|
|
payload[pos++] = '\0';
|
|
|
|
|
memcpy(payload + pos, s->sasl_user, ulen); pos += ulen;
|
|
|
|
|
payload[pos++] = '\0';
|
|
|
|
|
memcpy(payload + pos, s->sasl_pass, plen_pass); pos += plen_pass;
|
|
|
|
|
char enc[700];
|
|
|
|
|
base64_encode((unsigned char*)payload, total, enc);
|
|
|
|
|
srv_sendf(si, "AUTHENTICATE %s", enc);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (strcmp(m.cmd,"903")==0) {
|
|
|
|
@@ -841,7 +894,12 @@ static void handle_irc_line(int si, const char *raw) {
|
|
|
|
|
ev_simple(EV_STATUS, si, NULL, NULL, "[list] end"); return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (strcmp(m.cmd,"366")==0) return;
|
|
|
|
|
if (strcmp(m.cmd,"366")==0) {
|
|
|
|
|
/* end of NAMES — reset pending flag so next /names clears again */
|
|
|
|
|
const char *ch = m.nparams>=2 ? m.params[1] : (m.nparams>=1 ? m.params[0] : "");
|
|
|
|
|
ev_simple(EV_NAMES_END, si, NULL, ch, NULL);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* AWAY */
|
|
|
|
|
if (strcmp(m.cmd,"301")==0) {
|
|
|
|
@@ -904,6 +962,9 @@ static void srv_close(int si) {
|
|
|
|
|
|
|
|
|
|
static int srv_connect(int si) {
|
|
|
|
|
Server *s=&g_srv[si];
|
|
|
|
|
/* close any previous connection first (e.g. leftover from /connect) */
|
|
|
|
|
if (s->use_tls && s->ssl) { SSL_shutdown(s->ssl); SSL_free(s->ssl); s->ssl=NULL; }
|
|
|
|
|
if (s->sock>=0) { close(s->sock); s->sock=-1; }
|
|
|
|
|
char portstr[8]; snprintf(portstr,sizeof(portstr),"%d",s->port);
|
|
|
|
|
struct addrinfo hints,*res,*r;
|
|
|
|
|
memset(&hints,0,sizeof(hints));
|
|
|
|
@@ -914,7 +975,7 @@ static int srv_connect(int si) {
|
|
|
|
|
int fd=socket(r->ai_family,r->ai_socktype,r->ai_protocol);
|
|
|
|
|
if (fd<0) continue;
|
|
|
|
|
int flags=fcntl(fd,F_GETFL,0);
|
|
|
|
|
fcntl(fd,F_SETFL,flags|O_NONBLOCK);
|
|
|
|
|
if (flags>=0) fcntl(fd,F_SETFL,flags|O_NONBLOCK);
|
|
|
|
|
int rc=connect(fd,r->ai_addr,r->ai_addrlen);
|
|
|
|
|
if (rc==0||errno==EINPROGRESS) {
|
|
|
|
|
fd_set wfds; FD_ZERO(&wfds); FD_SET(fd,&wfds);
|
|
|
|
@@ -930,16 +991,20 @@ static int srv_connect(int si) {
|
|
|
|
|
freeaddrinfo(res);
|
|
|
|
|
if (s->sock<0) return -1;
|
|
|
|
|
int flags=fcntl(s->sock,F_GETFL,0);
|
|
|
|
|
fcntl(s->sock,F_SETFL,flags&~O_NONBLOCK);
|
|
|
|
|
if (flags>=0) fcntl(s->sock,F_SETFL,flags&~O_NONBLOCK);
|
|
|
|
|
|
|
|
|
|
if (s->use_tls) {
|
|
|
|
|
pthread_mutex_lock(&g_ssl_mutex);
|
|
|
|
|
if (!g_ssl_ctx) {
|
|
|
|
|
SSL_library_init(); SSL_load_error_strings();
|
|
|
|
|
g_ssl_ctx=SSL_CTX_new(TLS_client_method());
|
|
|
|
|
if (!g_ssl_ctx) return -1;
|
|
|
|
|
if (g_ssl_ctx) {
|
|
|
|
|
SSL_CTX_set_verify(g_ssl_ctx,SSL_VERIFY_PEER,NULL);
|
|
|
|
|
SSL_CTX_set_default_verify_paths(g_ssl_ctx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pthread_mutex_unlock(&g_ssl_mutex);
|
|
|
|
|
if (!g_ssl_ctx) return -1;
|
|
|
|
|
s->ssl=SSL_new(g_ssl_ctx);
|
|
|
|
|
SSL_set_fd(s->ssl,s->sock);
|
|
|
|
|
SSL_set_tlsext_host_name(s->ssl,s->host);
|
|
|
|
@@ -956,6 +1021,10 @@ static void *net_thread(void *arg) {
|
|
|
|
|
NetArg *na=(NetArg*)arg; int si=na->si; free(na);
|
|
|
|
|
Server *s=&g_srv[si];
|
|
|
|
|
|
|
|
|
|
/* snapshot config that may be changed concurrently by main thread */
|
|
|
|
|
char mynick[MAX_NICK];
|
|
|
|
|
strncpy(mynick, s->nick, MAX_NICK-1); mynick[MAX_NICK-1]='\0';
|
|
|
|
|
|
|
|
|
|
char buf[MAX_LINE];
|
|
|
|
|
snprintf(buf,sizeof(buf),"Connecting to %s:%d%s...",
|
|
|
|
|
s->host,s->port,s->use_tls?" (TLS)":"");
|
|
|
|
@@ -968,17 +1037,20 @@ static void *net_thread(void *arg) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int sasl_pending=(s->sasl_user[0]&&s->sasl_pass[0])?1:0;
|
|
|
|
|
if (sasl_pending)
|
|
|
|
|
srv_send_raw(si,"CAP REQ :sasl");
|
|
|
|
|
else {
|
|
|
|
|
srv_sendf(si,"NICK %s",s->nick);
|
|
|
|
|
srv_sendf(si,"USER %s 0 * :sirc",s->nick);
|
|
|
|
|
s->sasl_auth_sent = 0;
|
|
|
|
|
if (sasl_pending) {
|
|
|
|
|
/* CAP LS 302: proper negotiation, handler sends CAP REQ :sasl on LS reply */
|
|
|
|
|
srv_send_raw(si,"CAP LS 302");
|
|
|
|
|
} else {
|
|
|
|
|
srv_sendf(si,"NICK %s",mynick);
|
|
|
|
|
srv_sendf(si,"USER %s 0 * :sirc",mynick);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char readbuf[8192], linebuf[MAX_LINE*4];
|
|
|
|
|
int linelen=0;
|
|
|
|
|
time_t last_ping=time(NULL);
|
|
|
|
|
|
|
|
|
|
int intentional = 0;
|
|
|
|
|
while (!s->net_stop) {
|
|
|
|
|
if (time(NULL)-last_ping>PING_INTERVAL) {
|
|
|
|
|
srv_sendf(si,"PING :%s",s->host);
|
|
|
|
@@ -1012,8 +1084,11 @@ static void *net_thread(void *arg) {
|
|
|
|
|
last_ping=time(NULL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (s->net_stop) intentional = 1;
|
|
|
|
|
if (!intentional) {
|
|
|
|
|
srv_close(si);
|
|
|
|
|
ev_simple(EV_RECONNECT, si, NULL, NULL, NULL);
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1033,7 +1108,9 @@ static void *reconnect_thread(void *arg) {
|
|
|
|
|
static void start_net_thread(int si) {
|
|
|
|
|
g_srv[si].net_stop=0;
|
|
|
|
|
NetArg *na=malloc(sizeof(NetArg)); na->si=si;
|
|
|
|
|
pthread_create(&g_srv[si].net_tid, NULL, net_thread, na);
|
|
|
|
|
pthread_t tid;
|
|
|
|
|
pthread_create(&tid, NULL, net_thread, na);
|
|
|
|
|
pthread_detach(tid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void schedule_reconnect(int si) {
|
|
|
|
@@ -1068,13 +1145,21 @@ static int srv_alloc(void) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void srv_add_autojoin(int si, const char *chanlist) {
|
|
|
|
|
char tmp[512]; strncpy(tmp,chanlist,511);
|
|
|
|
|
char tmp[MAX_LINE]; strncpy(tmp, chanlist, MAX_LINE-1);
|
|
|
|
|
char *tok = strtok(tmp, ",");
|
|
|
|
|
while (tok) {
|
|
|
|
|
/* trim leading and trailing spaces */
|
|
|
|
|
while (*tok == ' ') tok++;
|
|
|
|
|
char *end = tok + strlen(tok) - 1;
|
|
|
|
|
while (end > tok && *end == ' ') *end-- = '\0';
|
|
|
|
|
if (*tok && g_srv[si].autojoin_count < MAX_AUTOJOIN) {
|
|
|
|
|
char ch[MAX_CHAN]; strncpy(ch, tok, MAX_CHAN-1);
|
|
|
|
|
if (ch[0] != '#') { memmove(ch+1, ch, strlen(ch)+1); ch[0]='#'; }
|
|
|
|
|
/* avoid duplicates */
|
|
|
|
|
int dup = 0;
|
|
|
|
|
for (int i = 0; i < g_srv[si].autojoin_count; i++)
|
|
|
|
|
if (strcasecmp(g_srv[si].autojoin[i], ch) == 0) { dup=1; break; }
|
|
|
|
|
if (!dup)
|
|
|
|
|
strncpy(g_srv[si].autojoin[g_srv[si].autojoin_count++], ch, MAX_CHAN-1);
|
|
|
|
|
}
|
|
|
|
|
tok = strtok(NULL, ",");
|
|
|
|
@@ -1082,7 +1167,7 @@ static void srv_add_autojoin(int si, const char *chanlist) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void load_config(const char *path) {
|
|
|
|
|
char candidates[2][MAX_HOST];
|
|
|
|
|
char candidates[2][MAX_HOST] = {{0},{0}};
|
|
|
|
|
const char *home=getenv("HOME");
|
|
|
|
|
if (home) {
|
|
|
|
|
snprintf(candidates[0],MAX_HOST,"%s/.sirc",home);
|
|
|
|
@@ -1402,6 +1487,10 @@ static void draw_input(void) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void draw_all(void) {
|
|
|
|
|
if (g_active < 0 || g_chan_count == 0) {
|
|
|
|
|
/* no channels yet — only draw input line */
|
|
|
|
|
draw_input(); doupdate(); return;
|
|
|
|
|
}
|
|
|
|
|
draw_status_bar(); draw_channels(); draw_chat();
|
|
|
|
|
draw_users(); draw_input(); doupdate();
|
|
|
|
|
}
|
|
|
|
@@ -1450,15 +1539,20 @@ static void build_windows(void) {
|
|
|
|
|
/* ── event handler ─────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
static void do_rejoin_channels(int si) {
|
|
|
|
|
int any=0;
|
|
|
|
|
for (int i=0;i<g_chan_count;i++) {
|
|
|
|
|
if (g_chans[i].srv==si && g_chans[i].name[0]=='#') {
|
|
|
|
|
srv_sendf(si,"JOIN %s",g_chans[i].name); any=1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!any) {
|
|
|
|
|
/* always join every configured autojoin channel */
|
|
|
|
|
for (int j = 0; j < g_srv[si].autojoin_count; j++)
|
|
|
|
|
srv_sendf(si, "JOIN %s", g_srv[si].autojoin[j]);
|
|
|
|
|
|
|
|
|
|
/* also rejoin any channels open from a previous session that aren't
|
|
|
|
|
in the autojoin list (e.g. channels joined manually before disconnect) */
|
|
|
|
|
for (int i = 0; i < g_chan_count; i++) {
|
|
|
|
|
if (g_chans[i].srv != si || g_chans[i].name[0] != '#') continue;
|
|
|
|
|
int already = 0;
|
|
|
|
|
for (int j = 0; j < g_srv[si].autojoin_count; j++)
|
|
|
|
|
if (strcasecmp(g_srv[si].autojoin[j], g_chans[i].name) == 0)
|
|
|
|
|
{ already = 1; break; }
|
|
|
|
|
if (!already)
|
|
|
|
|
srv_sendf(si, "JOIN %s", g_chans[i].name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1489,6 +1583,7 @@ static void handle_event(const Event *ev) {
|
|
|
|
|
if (ev->chan[0]) {
|
|
|
|
|
int ci=chan_find(si,ev->chan);
|
|
|
|
|
if (ci<0) ci=chan_add(si,ev->chan);
|
|
|
|
|
if (ci<0) ci=srv_status_chan(si);
|
|
|
|
|
chan_addline(&g_chans[ci],F_STATUS,ev->text);
|
|
|
|
|
if (ci==g_active) g_chans[ci].unread=0;
|
|
|
|
|
} else {
|
|
|
|
@@ -1505,6 +1600,7 @@ static void handle_event(const Event *ev) {
|
|
|
|
|
const char *target=ev->chan, *text=ev->text;
|
|
|
|
|
const char *dest=(target[0]=='#')?target:ev->nick;
|
|
|
|
|
int ci=chan_find(si,dest); if(ci<0) ci=chan_add(si,dest);
|
|
|
|
|
if (ci<0) ci=srv_status_chan(si);
|
|
|
|
|
|
|
|
|
|
if (text[0]=='\x01'&&strncmp(text+1,"ACTION",6)==0) {
|
|
|
|
|
const char *act=text+8;
|
|
|
|
@@ -1530,6 +1626,7 @@ static void handle_event(const Event *ev) {
|
|
|
|
|
|
|
|
|
|
case EV_JOIN: {
|
|
|
|
|
int ci=chan_find(si,ev->chan); if(ci<0) ci=chan_add(si,ev->chan);
|
|
|
|
|
if (ci<0) ci=srv_status_chan(si);
|
|
|
|
|
if (strcmp(ev->nick,s->nick)==0) {
|
|
|
|
|
g_chans[ci].user_count=0;
|
|
|
|
|
g_active=ci;
|
|
|
|
@@ -1539,6 +1636,7 @@ static void handle_event(const Event *ev) {
|
|
|
|
|
char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"-> %s joined",ev->nick);
|
|
|
|
|
chan_addline(&g_chans[ci],F_JOIN,msg);
|
|
|
|
|
chan_adduser(&g_chans[ci], 0, ev->nick);
|
|
|
|
|
chan_sort_users(&g_chans[ci]);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
@@ -1556,8 +1654,11 @@ static void handle_event(const Event *ev) {
|
|
|
|
|
for (int i=0;i<g_chan_count;i++) {
|
|
|
|
|
if (g_chans[i].srv!=si) continue;
|
|
|
|
|
int found=0;
|
|
|
|
|
for (int j=0;j<g_chans[i].user_count;j++)
|
|
|
|
|
if (strcasecmp(g_chans[i].users[j],ev->nick)==0){found=1;break;}
|
|
|
|
|
for (int j=0;j<g_chans[i].user_count;j++) {
|
|
|
|
|
const char *sn = g_chans[i].users[j];
|
|
|
|
|
if (*sn=='@'||*sn=='+'||*sn=='~'||*sn=='&'||*sn=='%'||*sn==' ') sn++;
|
|
|
|
|
if (strcasecmp(sn,ev->nick)==0){found=1;break;}
|
|
|
|
|
}
|
|
|
|
|
if (found) {
|
|
|
|
|
char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<- %s quit (%s)",ev->nick,ev->text);
|
|
|
|
|
chan_addline(&g_chans[i],F_JOIN,msg);
|
|
|
|
@@ -1591,6 +1692,11 @@ static void handle_event(const Event *ev) {
|
|
|
|
|
|
|
|
|
|
case EV_NAMES: {
|
|
|
|
|
int ci=chan_find(si,ev->chan); if(ci<0) break;
|
|
|
|
|
/* first nick of a fresh NAMES reply — wipe the stale list */
|
|
|
|
|
if (!g_chans[ci].names_pending) {
|
|
|
|
|
g_chans[ci].user_count = 0;
|
|
|
|
|
g_chans[ci].names_pending = 1;
|
|
|
|
|
}
|
|
|
|
|
chan_adduser(&g_chans[ci], ev->extra[0], ev->nick);
|
|
|
|
|
chan_sort_users(&g_chans[ci]);
|
|
|
|
|
break;
|
|
|
|
@@ -1601,7 +1707,8 @@ static void handle_event(const Event *ev) {
|
|
|
|
|
char msg[MAX_LINE];
|
|
|
|
|
snprintf(msg,sizeof(msg),"X %s was kicked by %s (%s)",ev->extra,ev->nick,ev->text);
|
|
|
|
|
chan_addline(&g_chans[ci],F_JOIN,msg);
|
|
|
|
|
chan_removeuser(&g_chans[ci],ev->extra);
|
|
|
|
|
if (strcmp(ev->extra,s->nick)==0) chan_remove(ci);
|
|
|
|
|
else chan_removeuser(&g_chans[ci],ev->extra);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1611,6 +1718,12 @@ static void handle_event(const Event *ev) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case EV_NAMES_END: {
|
|
|
|
|
int ci=chan_find(si,ev->chan); if(ci<0) break;
|
|
|
|
|
g_chans[ci].names_pending = 0;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1694,16 +1807,18 @@ static void process_input(const char *text) {
|
|
|
|
|
if (strcmp(cmd,"/join")==0) {
|
|
|
|
|
char ch[MAX_CHAN]; strncpy(ch,arg[0]?arg:g_srv[si].autojoin_count?g_srv[si].autojoin[0]:"#general",MAX_CHAN-1);
|
|
|
|
|
if(ch[0]!='#'){memmove(ch+1,ch,strlen(ch)+1);ch[0]='#';}
|
|
|
|
|
chan_add(si,ch);
|
|
|
|
|
if(chan_add(si,ch)<0) srv_status_msg(si,"Channel list full.");
|
|
|
|
|
if(s->connected) srv_sendf(si,"JOIN %s",ch);
|
|
|
|
|
else srv_status_msg(si,"[not connected]");
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/part")==0) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
const char *ch=arg[0]?arg:g_chans[g_active].name;
|
|
|
|
|
if(s->connected) srv_sendf(si,"PART %s :bye",ch);
|
|
|
|
|
else srv_status_msg(si,"[not connected]");
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/cycle")==0) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
const char *ch=arg[0]?arg:g_chans[g_active].name;
|
|
|
|
|
if(ch[0]!='#'){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
|
|
@@ -1721,13 +1836,14 @@ static void process_input(const char *text) {
|
|
|
|
|
} else if (strcmp(cmd,"/msg")==0) {
|
|
|
|
|
char tgt[MAX_NICK],msg[MAX_LINE];
|
|
|
|
|
if(sscanf(arg,"%63s %[^\n]",tgt,msg)==2) {
|
|
|
|
|
chan_add(si,tgt);
|
|
|
|
|
int ci=chan_add(si,tgt);
|
|
|
|
|
if(ci<0) ci=srv_status_chan(si);
|
|
|
|
|
if(s->connected) srv_sendf(si,"PRIVMSG %s :%s",tgt,msg);
|
|
|
|
|
int ci=chan_find(si,tgt);
|
|
|
|
|
if(ci>=0){ char full[MAX_LINE]; snprintf(full,sizeof(full),"<%s> %s",s->nick,msg); chan_addline(&g_chans[ci],F_ME,full); }
|
|
|
|
|
{ char full[MAX_LINE]; snprintf(full,sizeof(full),"<%s> %s",s->nick,msg); chan_addline(&g_chans[ci],F_ME,full); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/me")==0) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
const char *cn=g_chans[g_active].name;
|
|
|
|
|
if(cn[0]=='*'){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
|
|
@@ -1753,6 +1869,7 @@ static void process_input(const char *text) {
|
|
|
|
|
} else srv_status_msg(si,"Usage: /ctcp <nick> <command>");
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/names")==0) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
const char *ch=arg[0]?arg:g_chans[g_active].name;
|
|
|
|
|
if(ch[0]=='*'){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
if(s->connected) srv_sendf(si,"NAMES %s",ch);
|
|
|
|
@@ -1771,6 +1888,7 @@ static void process_input(const char *text) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/topic")==0) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
const char *ch=g_chans[g_active].name;
|
|
|
|
|
if(ch[0]=='*'){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
|
|
@@ -1783,6 +1901,7 @@ static void process_input(const char *text) {
|
|
|
|
|
srv_sendf(si,"WHOIS %s",arg);
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/who")==0) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
const char *tgt=arg[0]?arg:g_chans[g_active].name;
|
|
|
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
|
|
|
srv_sendf(si,"WHO %s",tgt);
|
|
|
|
@@ -1801,6 +1920,7 @@ static void process_input(const char *text) {
|
|
|
|
|
srv_send_raw(si,"AWAY");
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/invite")==0) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count){srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
char tgt[MAX_NICK],ch[MAX_CHAN]; ch[0]='\0';
|
|
|
|
|
if(sscanf(arg,"%63s %63s",tgt,ch)<1){srv_status_msg(si,"Usage: /invite <nick> [#chan]");return;}
|
|
|
|
|
if(!ch[0]) strncpy(ch,g_chans[g_active].name,MAX_CHAN-1);
|
|
|
|
@@ -1818,7 +1938,11 @@ static void process_input(const char *text) {
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/mode")==0) {
|
|
|
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
|
|
|
if(!arg[0]) srv_sendf(si,"MODE %s",g_chans[g_active].name);
|
|
|
|
|
if(!arg[0]) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count||g_chans[g_active].name[0]=='*')
|
|
|
|
|
{srv_status_msg(si,"Not in a channel.");return;}
|
|
|
|
|
srv_sendf(si,"MODE %s",g_chans[g_active].name);
|
|
|
|
|
}
|
|
|
|
|
else srv_sendf(si,"MODE %s",arg);
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/raw")==0||strcmp(cmd,"/quote")==0) {
|
|
|
|
@@ -1837,7 +1961,11 @@ static void process_input(const char *text) {
|
|
|
|
|
g_srv[nsi].port=newport;
|
|
|
|
|
/* inherit nick from current server */
|
|
|
|
|
strncpy(g_srv[nsi].nick,s->nick,MAX_NICK-1);
|
|
|
|
|
chan_add(nsi,"*status*");
|
|
|
|
|
if(chan_add(nsi,"*status*")<0) {
|
|
|
|
|
srv_status_msg(si,"Channel list full.");
|
|
|
|
|
g_srv_count--; /* undo srv_alloc */
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
start_net_thread(nsi);
|
|
|
|
|
srv_status_fmt(nsi,"Connecting to %s:%d...",newhost,newport);
|
|
|
|
|
|
|
|
|
@@ -1859,6 +1987,7 @@ static void process_input(const char *text) {
|
|
|
|
|
else srv_status_fmt(si,"%s is not ignored",arg);
|
|
|
|
|
|
|
|
|
|
} else if (strcmp(cmd,"/clear")==0) {
|
|
|
|
|
if(g_active<0||g_active>=g_chan_count) return;
|
|
|
|
|
g_chans[g_active].line_count=0;
|
|
|
|
|
g_chans[g_active].line_head=0;
|
|
|
|
|
g_chans[g_active].scroll=0;
|
|
|
|
@@ -1925,10 +2054,17 @@ static void handle_key(int key) {
|
|
|
|
|
if (key=='\t') { tab_complete(); return; }
|
|
|
|
|
tab_reset();
|
|
|
|
|
|
|
|
|
|
if (g_active<0||g_active>=g_chan_count) {
|
|
|
|
|
/* no channels — only accept resize and printable input */
|
|
|
|
|
if (key=='\n'||key=='\r'||key==KEY_ENTER) {
|
|
|
|
|
g_input[0]='\0'; g_input_len=0; g_input_cur=0;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Channel *ch=&g_chans[g_active];
|
|
|
|
|
|
|
|
|
|
if (key==14) { g_active=chan_next(g_active,1); g_chans[g_active].unread=0; g_chans[g_active].mention=0; return; }
|
|
|
|
|
if (key==16) { g_active=chan_next(g_active,-1); g_chans[g_active].unread=0; g_chans[g_active].mention=0; return; }
|
|
|
|
|
if (key==14) { g_active=chan_next(g_active,1); if(g_active>=0){g_chans[g_active].unread=0; g_chans[g_active].mention=0;} return; }
|
|
|
|
|
if (key==16) { g_active=chan_next(g_active,-1); if(g_active>=0){g_chans[g_active].unread=0; g_chans[g_active].mention=0;} return; }
|
|
|
|
|
|
|
|
|
|
if (key==KEY_PPAGE) { int ch2; getmaxyx(win_chat,ch2,(int){0}); ch->scroll+=ch2/2; return; }
|
|
|
|
|
if (key==KEY_NPAGE) { int ch2; getmaxyx(win_chat,ch2,(int){0}); ch->scroll-=ch2/2; if(ch->scroll<0)ch->scroll=0; return; }
|
|
|
|
@@ -2044,8 +2180,11 @@ int main(int argc, char **argv) {
|
|
|
|
|
if (cli_chan && !cli_host) srv_add_autojoin(i, cli_chan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* create status channels */
|
|
|
|
|
for(int i=0;i<g_srv_count;i++) chan_add(i,"*status*");
|
|
|
|
|
/* create status channels — failures non-fatal; srv_status_chan falls back */
|
|
|
|
|
for(int i=0;i<g_srv_count;i++) {
|
|
|
|
|
if (chan_add(i,"*status*") < 0) break;
|
|
|
|
|
}
|
|
|
|
|
if (g_active < 0 && g_chan_count > 0) g_active = 0;
|
|
|
|
|
|
|
|
|
|
/* event pipe */
|
|
|
|
|
if(pipe(g_evpipe)<0){perror("pipe");return 1;}
|
|
|
|
|