2117 lines
78 KiB
C
2117 lines
78 KiB
C
/*
|
|
* sirc.c — multi-server terminal IRC client
|
|
*
|
|
* Usage: sirc [options]
|
|
*
|
|
* Config (~/.sirc):
|
|
* # Default server (applied when no [server] block precedes it)
|
|
* nick = mynick
|
|
* ignore = badbot,spammer
|
|
*
|
|
* [server]
|
|
* host = irc.libera.chat
|
|
* port = 6697
|
|
* nick = mynick
|
|
* channel = #python,#linux
|
|
* tls = true
|
|
* sasl_user = mynick
|
|
* sasl_pass = hunter2
|
|
*
|
|
* [server]
|
|
* host = irc.oftc.net
|
|
* port = 6697
|
|
* tls = true
|
|
* channel = #debian
|
|
*
|
|
* CLI adds one server on top of whatever the config defines:
|
|
* --host / --port / --nick / --channel / --tls / --no-tls
|
|
* --sasl-user / --sasl-pass / --config
|
|
*
|
|
* Commands:
|
|
* /join #chan join channel (on current server)
|
|
* /part [#chan] leave channel
|
|
* /cycle [#chan] part and rejoin
|
|
* /nick NAME change nick
|
|
* /msg NICK TEXT private message
|
|
* /me TEXT action
|
|
* /notice TGT TEXT send notice
|
|
* /ctcp NICK CMD CTCP request
|
|
* /names [#chan] list users
|
|
* /topic [text] get/set topic
|
|
* /whois NICK whois lookup
|
|
* /who [target] who query
|
|
* /list [pattern] list channels on server
|
|
* /away [msg] set away
|
|
* /back clear away
|
|
* /invite NICK [chan] invite to channel
|
|
* /kick NICK [reason] kick from channel
|
|
* /mode [target] [m] get/set mode
|
|
* /server HOST [port] connect to new server
|
|
* /raw <line> send raw IRC line (alias /quote)
|
|
* /ignore [nick] ignore nick (no arg = list)
|
|
* /unignore NICK unignore nick
|
|
* /clear clear scrollback
|
|
* /connect reconnect current server
|
|
* /quit [msg] disconnect and exit
|
|
* /help show help
|
|
*
|
|
* Keys:
|
|
* Tab nick completion (cycle)
|
|
* Ctrl+N/P next/prev channel (skips server headers)
|
|
* PgUp/PgDn scroll chat
|
|
* Ctrl+W delete word left
|
|
* Up/Down input history
|
|
*/
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
#define _DEFAULT_SOURCE
|
|
#define _GNU_SOURCE
|
|
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdarg.h>
|
|
#include <ctype.h>
|
|
#include <time.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <sys/socket.h>
|
|
#include <netdb.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <ncurses.h>
|
|
#include <openssl/ssl.h>
|
|
#include <openssl/err.h>
|
|
|
|
/* ── constants ─────────────────────────────────────────────────────────────── */
|
|
|
|
#define MAX_NICK 64
|
|
#define MAX_CHAN 64
|
|
#define MAX_HOST 256
|
|
#define MAX_LINE 512
|
|
#define MAX_INPUT 480
|
|
#define MAX_HISTORY 500
|
|
#define MAX_SERVERS 8
|
|
#define MAX_CHANS_TOTAL 128 /* across all servers */
|
|
#define MAX_USERS 512
|
|
#define MAX_IGNORE 64
|
|
#define MAX_HIST_LINES 256
|
|
#define MAX_AUTOJOIN 16
|
|
#define PING_INTERVAL 90
|
|
#define CHAN_W 18
|
|
#define USER_W 16
|
|
#define RECONNECT_DELAY 5
|
|
|
|
/* colour pair ids */
|
|
#define C_BORDER 1
|
|
#define C_HEADER 2
|
|
#define C_ME 4
|
|
#define C_JOIN 5
|
|
#define C_MENTION 6
|
|
#define C_STATUS 7
|
|
#define C_INPUT 8
|
|
#define C_CHAN_SEL 9
|
|
#define C_CHAN 10
|
|
#define C_UNREAD 11
|
|
#define C_MENTION_CHAN 12
|
|
#define C_URL 13
|
|
#define C_ACTION 14
|
|
#define C_SERVER_HDR 15 /* server name row in channel list */
|
|
#define C_NICK_BASE 16
|
|
#define C_NICK_COUNT 8
|
|
|
|
/* line flags */
|
|
#define F_MSG 0
|
|
#define F_ME 1
|
|
#define F_MENTION 2
|
|
#define F_JOIN 3
|
|
#define F_STATUS 4
|
|
#define F_ACTION 5
|
|
|
|
/* ── data structures ───────────────────────────────────────────────────────── */
|
|
|
|
typedef struct {
|
|
char ts[8];
|
|
int flag;
|
|
char text[MAX_LINE];
|
|
} Line;
|
|
|
|
typedef struct {
|
|
char name[MAX_CHAN];
|
|
int srv; /* index of owning server */
|
|
Line lines[MAX_HISTORY];
|
|
int line_head;
|
|
int line_count;
|
|
char users[MAX_USERS][MAX_NICK];
|
|
int user_count;
|
|
int unread;
|
|
int mention;
|
|
int scroll;
|
|
char topic[MAX_LINE];
|
|
} Channel;
|
|
|
|
typedef struct {
|
|
char text[MAX_INPUT];
|
|
} HistEntry;
|
|
|
|
/* IRC event types — each carries a server index so the UI routes correctly */
|
|
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
|
|
} EvType;
|
|
|
|
typedef struct {
|
|
EvType type;
|
|
int srv; /* server index */
|
|
char nick[MAX_NICK];
|
|
char chan[MAX_CHAN];
|
|
char text[MAX_LINE];
|
|
char extra[MAX_NICK];
|
|
} Event;
|
|
|
|
/* per-server state */
|
|
typedef struct {
|
|
/* config */
|
|
char host[MAX_HOST];
|
|
int port;
|
|
char nick[MAX_NICK];
|
|
char autojoin[MAX_AUTOJOIN][MAX_CHAN];
|
|
int autojoin_count;
|
|
int use_tls;
|
|
char sasl_user[MAX_NICK];
|
|
char sasl_pass[256];
|
|
|
|
/* runtime */
|
|
int connected;
|
|
volatile int net_stop;
|
|
pthread_t net_tid;
|
|
pthread_mutex_t send_lock;
|
|
int sock;
|
|
SSL *ssl;
|
|
|
|
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];
|
|
} Server;
|
|
|
|
/* ── globals ───────────────────────────────────────────────────────────────── */
|
|
|
|
static Server g_srv[MAX_SERVERS];
|
|
static int g_srv_count = 0;
|
|
static SSL_CTX *g_ssl_ctx = NULL;
|
|
|
|
/* 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 */
|
|
|
|
/* global ignore list */
|
|
static char g_ignore[MAX_IGNORE][MAX_NICK];
|
|
static int g_ignore_count = 0;
|
|
|
|
/* input */
|
|
static char g_input[MAX_INPUT];
|
|
static int g_input_len = 0;
|
|
static int g_input_cur = 0;
|
|
static HistEntry g_hist[MAX_HIST_LINES];
|
|
static int g_hist_count = 0;
|
|
static int g_hist_pos = -1;
|
|
|
|
/* tab completion */
|
|
static char g_tab_matches[MAX_USERS][MAX_NICK];
|
|
static int g_tab_count = 0;
|
|
static int g_tab_idx = 0;
|
|
static int g_tab_word_start = 0;
|
|
static int g_tab_active = 0;
|
|
|
|
/* single event pipe the UI polls — all servers write here */
|
|
static int g_evpipe[2];
|
|
|
|
/* curses windows */
|
|
static WINDOW *win_chan = NULL;
|
|
static WINDOW *win_chat = NULL;
|
|
static WINDOW *win_users = NULL;
|
|
static WINDOW *win_input = NULL;
|
|
static WINDOW *win_status = NULL;
|
|
|
|
/* ── utility ───────────────────────────────────────────────────────────────── */
|
|
|
|
static void str_upper(char *dst, const char *src, int n) {
|
|
int i;
|
|
for (i = 0; i < n-1 && src[i]; i++)
|
|
dst[i] = toupper((unsigned char)src[i]);
|
|
dst[i] = '\0';
|
|
}
|
|
|
|
static int str_icase_starts(const char *hay, const char *needle) {
|
|
while (*needle) {
|
|
if (tolower((unsigned char)*hay) != tolower((unsigned char)*needle))
|
|
return 0;
|
|
hay++; needle++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int nick_colour(const char *nick) {
|
|
unsigned int h = 0;
|
|
for (; *nick; nick++) h = (h * 31 + (unsigned char)*nick) & 0xFFFF;
|
|
return C_NICK_BASE + (h % C_NICK_COUNT);
|
|
}
|
|
|
|
static void timestamp(char *buf, int n) {
|
|
time_t t = time(NULL);
|
|
strftime(buf, n, "%H:%M", localtime(&t));
|
|
}
|
|
|
|
/* ── channel management ────────────────────────────────────────────────────── */
|
|
|
|
static int chan_find(int srv, const char *name) {
|
|
for (int i = 0; i < g_chan_count; i++)
|
|
if (g_chans[i].srv == srv && strcasecmp(g_chans[i].name, name) == 0)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
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;
|
|
i = g_chan_count++;
|
|
memset(&g_chans[i], 0, sizeof(Channel));
|
|
strncpy(g_chans[i].name, name, MAX_CHAN-1);
|
|
g_chans[i].srv = srv;
|
|
return i;
|
|
}
|
|
|
|
static void chan_remove(int idx) {
|
|
if (idx < 0 || idx >= g_chan_count) return;
|
|
for (int i = idx; i < g_chan_count-1; i++)
|
|
g_chans[i] = g_chans[i+1];
|
|
g_chan_count--;
|
|
if (g_active >= g_chan_count) g_active = g_chan_count - 1;
|
|
if (g_active < 0) g_active = 0;
|
|
}
|
|
|
|
static Line *chan_line(Channel *ch, int idx) {
|
|
if (idx < 0 || idx >= ch->line_count) return NULL;
|
|
int real = (ch->line_head - ch->line_count + idx + MAX_HISTORY) % MAX_HISTORY;
|
|
return &ch->lines[real];
|
|
}
|
|
|
|
/* strip IRC formatting: \x02 bold, \x03 colour, \x0f reset,
|
|
\x1d italic, \x1f underline, \x1e strikethrough, \x11 monospace */
|
|
static void strip_irc_fmt(const char *in, char *out, int outlen) {
|
|
int j = 0;
|
|
for (int i = 0; in[i] && j < outlen-1; i++) {
|
|
unsigned char c = (unsigned char)in[i];
|
|
if (c==0x02||c==0x0f||c==0x1d||c==0x1f||c==0x1e||c==0x11)
|
|
continue;
|
|
if (c == 0x03) { /* colour: \x03[fg[,bg]] — skip digits */
|
|
i++;
|
|
if (in[i] && isdigit((unsigned char)in[i])) i++;
|
|
if (in[i] && isdigit((unsigned char)in[i])) i++;
|
|
if (in[i] == ',') {
|
|
i++;
|
|
if (in[i] && isdigit((unsigned char)in[i])) i++;
|
|
if (in[i] && isdigit((unsigned char)in[i])) i++;
|
|
}
|
|
i--;
|
|
continue;
|
|
}
|
|
out[j++] = (char)c;
|
|
}
|
|
out[j] = '\0';
|
|
}
|
|
|
|
static void chan_addline(Channel *ch, int flag, const char *text) {
|
|
char ts[8]; timestamp(ts, sizeof(ts));
|
|
Line *l = &ch->lines[ch->line_head % MAX_HISTORY];
|
|
strncpy(l->ts, ts, sizeof(l->ts)-1);
|
|
strip_irc_fmt(text, l->text, MAX_LINE);
|
|
l->flag = flag;
|
|
ch->line_head = (ch->line_head + 1) % MAX_HISTORY;
|
|
if (ch->line_count < MAX_HISTORY) ch->line_count++;
|
|
if (flag != F_STATUS) ch->unread++;
|
|
}
|
|
|
|
/* get the status channel for a server (create if needed) */
|
|
static int srv_status_chan(int srv) {
|
|
return chan_add(srv, "*status*");
|
|
}
|
|
|
|
static void srv_status_msg(int srv, const char *msg) {
|
|
int ci = srv_status_chan(srv);
|
|
chan_addline(&g_chans[ci], F_STATUS, msg);
|
|
}
|
|
|
|
static void srv_status_fmt(int srv, const char *fmt, ...) {
|
|
char buf[MAX_LINE];
|
|
va_list ap; va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
srv_status_msg(srv, buf);
|
|
}
|
|
|
|
/* Store user as "<prefix><nick>" where prefix is @, +, ~, &, % or space.
|
|
Lookup is always by bare nick (skip leading mode char). */
|
|
static int user_find(Channel *ch, const char *nick) {
|
|
for (int i = 0; i < ch->user_count; i++) {
|
|
const char *stored = ch->users[i];
|
|
/* skip stored mode prefix */
|
|
if (*stored == '@' || *stored == '+' || *stored == '~' ||
|
|
*stored == '&' || *stored == '%' || *stored == ' ')
|
|
stored++;
|
|
if (strcasecmp(stored, nick) == 0) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void chan_adduser(Channel *ch, char mode, const char *nick) {
|
|
int idx = user_find(ch, nick);
|
|
char entry[MAX_NICK];
|
|
entry[0] = mode ? mode : ' ';
|
|
strncpy(entry+1, nick, MAX_NICK-2);
|
|
if (idx >= 0) {
|
|
/* update mode in place */
|
|
strncpy(ch->users[idx], entry, MAX_NICK-1);
|
|
return;
|
|
}
|
|
if (ch->user_count >= MAX_USERS) return;
|
|
strncpy(ch->users[ch->user_count++], entry, MAX_NICK-1);
|
|
}
|
|
|
|
static void chan_removeuser(Channel *ch, const char *nick) {
|
|
int idx = user_find(ch, nick);
|
|
if (idx < 0) return;
|
|
for (int j = idx; j < ch->user_count-1; j++)
|
|
memcpy(ch->users[j], ch->users[j+1], MAX_NICK);
|
|
ch->user_count--;
|
|
}
|
|
|
|
static int mode_rank(char c) {
|
|
/* lower = higher privilege */
|
|
switch (c) {
|
|
case '~': return 0; /* founder/owner */
|
|
case '&': return 1; /* protected */
|
|
case '@': return 2; /* op */
|
|
case '%': return 3; /* halfop */
|
|
case '+': return 4; /* voice */
|
|
default: return 5; /* regular */
|
|
}
|
|
}
|
|
|
|
static int user_cmp(const void *a, const void *b) {
|
|
const char *sa = (const char *)a;
|
|
const char *sb = (const char *)b;
|
|
int ra = mode_rank(*sa);
|
|
int rb = mode_rank(*sb);
|
|
if (ra != rb) return ra - rb;
|
|
/* same rank: sort by nick (skip mode prefix) */
|
|
const char *na = (*sa == ' ' || mode_rank(*sa) < 5) ? sa+1 : sa;
|
|
const char *nb = (*sb == ' ' || mode_rank(*sb) < 5) ? sb+1 : sb;
|
|
return strcasecmp(na, nb);
|
|
}
|
|
|
|
static void chan_sort_users(Channel *ch) {
|
|
qsort(ch->users, ch->user_count, MAX_NICK, user_cmp);
|
|
}
|
|
|
|
/* next/prev channel index that is NOT a *status* channel (unless it's the
|
|
only kind), skipping nothing — just a linear wrap */
|
|
/* Build the visual order of channels (same traversal as draw_channels).
|
|
Returns the count; fills idx[] with g_chans indices in display order. */
|
|
static int visual_order(int *idx, int max) {
|
|
int n = 0;
|
|
for (int si = 0; si < g_srv_count && n < max; si++) {
|
|
/* *status* first */
|
|
for (int ci = 0; ci < g_chan_count && n < max; ci++) {
|
|
if (g_chans[ci].srv != si) continue;
|
|
if (strcmp(g_chans[ci].name, "*status*") == 0)
|
|
idx[n++] = ci;
|
|
}
|
|
/* then regular channels */
|
|
for (int ci = 0; ci < g_chan_count && n < max; ci++) {
|
|
if (g_chans[ci].srv != si) continue;
|
|
if (strcmp(g_chans[ci].name, "*status*") != 0)
|
|
idx[n++] = ci;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static int chan_next(int cur, int dir) {
|
|
if (g_chan_count <= 1) return cur;
|
|
int idx[MAX_CHANS_TOTAL];
|
|
int n = visual_order(idx, MAX_CHANS_TOTAL);
|
|
if (n == 0) return cur;
|
|
/* find cur in visual order */
|
|
int pos = 0;
|
|
for (int i = 0; i < n; i++)
|
|
if (idx[i] == cur) { pos = i; break; }
|
|
pos = (pos + dir + n) % n;
|
|
return idx[pos];
|
|
}
|
|
|
|
/* ── ignore list ───────────────────────────────────────────────────────────── */
|
|
|
|
static int is_ignored(const char *nick) {
|
|
for (int i = 0; i < g_ignore_count; i++)
|
|
if (strcasecmp(g_ignore[i], nick) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static void ignore_add(const char *nick) {
|
|
if (is_ignored(nick) || g_ignore_count >= MAX_IGNORE) return;
|
|
strncpy(g_ignore[g_ignore_count++], nick, MAX_NICK-1);
|
|
}
|
|
|
|
static void ignore_remove(const char *nick) {
|
|
for (int i = 0; i < g_ignore_count; i++) {
|
|
if (strcasecmp(g_ignore[i], nick) == 0) {
|
|
for (int j = i; j < g_ignore_count-1; j++)
|
|
memcpy(g_ignore[j], g_ignore[j+1], MAX_NICK);
|
|
g_ignore_count--;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── event pipe ────────────────────────────────────────────────────────────── */
|
|
|
|
static void ev_push(const Event *ev) {
|
|
write(g_evpipe[1], ev, sizeof(Event));
|
|
}
|
|
|
|
static void ev_simple(EvType type, int srv, const char *nick,
|
|
const char *chan, const char *text) {
|
|
Event ev; memset(&ev, 0, sizeof(ev));
|
|
ev.type = type; ev.srv = srv;
|
|
if (nick) strncpy(ev.nick, nick, MAX_NICK-1);
|
|
if (chan) strncpy(ev.chan, chan, MAX_CHAN-1);
|
|
if (text) strncpy(ev.text, text, MAX_LINE-1);
|
|
ev_push(&ev);
|
|
}
|
|
|
|
/* ── network send ──────────────────────────────────────────────────────────── */
|
|
|
|
static int srv_send_raw(int si, const char *line) {
|
|
char buf[MAX_LINE];
|
|
int n = snprintf(buf, sizeof(buf), "%s\r\n", line);
|
|
if (n <= 0 || n >= (int)sizeof(buf)) return -1;
|
|
Server *s = &g_srv[si];
|
|
pthread_mutex_lock(&s->send_lock);
|
|
int r;
|
|
if (s->use_tls && s->ssl)
|
|
r = SSL_write(s->ssl, buf, n);
|
|
else
|
|
r = send(s->sock, buf, n, 0);
|
|
pthread_mutex_unlock(&s->send_lock);
|
|
return r > 0 ? 0 : -1;
|
|
}
|
|
|
|
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);
|
|
va_end(ap);
|
|
srv_send_raw(si, buf);
|
|
}
|
|
|
|
/* ── IRC parser ────────────────────────────────────────────────────────────── */
|
|
|
|
typedef struct {
|
|
char prefix[MAX_LINE];
|
|
char cmd[32];
|
|
char params[16][MAX_LINE];
|
|
int nparams;
|
|
char trail[MAX_LINE];
|
|
} IrcMsg;
|
|
|
|
static void nick_from_prefix(const char *prefix, char *nick, int n) {
|
|
const char *bang = strchr(prefix, '!');
|
|
if (bang) {
|
|
int len = (int)(bang - prefix);
|
|
if (len >= n) len = n-1;
|
|
memcpy(nick, prefix, len); nick[len] = '\0';
|
|
} else {
|
|
strncpy(nick, prefix, n-1); nick[n-1] = '\0';
|
|
}
|
|
}
|
|
|
|
static void parse_irc(const char *raw, IrcMsg *m) {
|
|
memset(m, 0, sizeof(*m));
|
|
const char *p = raw;
|
|
if (*p == ':') {
|
|
p++;
|
|
const char *end = strchr(p, ' '); if (!end) return;
|
|
int len = (int)(end-p); if (len>=(int)sizeof(m->prefix)) len=sizeof(m->prefix)-1;
|
|
memcpy(m->prefix, p, len); p = end+1;
|
|
}
|
|
{
|
|
const char *end = strchr(p, ' ');
|
|
int len = end ? (int)(end-p) : (int)strlen(p);
|
|
if (len>=(int)sizeof(m->cmd)) len=sizeof(m->cmd)-1;
|
|
memcpy(m->cmd, p, len); str_upper(m->cmd, m->cmd, sizeof(m->cmd));
|
|
if (!end) return; p = end+1;
|
|
}
|
|
while (*p) {
|
|
if (*p == ':') { strncpy(m->trail, p+1, MAX_LINE-1); break; }
|
|
const char *end = strchr(p, ' ');
|
|
int len = end ? (int)(end-p) : (int)strlen(p);
|
|
if (m->nparams < 16) {
|
|
if (len>=MAX_LINE) len=MAX_LINE-1;
|
|
memcpy(m->params[m->nparams], p, len); m->nparams++;
|
|
}
|
|
if (!end) break; p = end+1;
|
|
}
|
|
}
|
|
|
|
/* ── base64 for SASL ───────────────────────────────────────────────────────── */
|
|
|
|
static const char b64tab[] =
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
static void base64_encode(const unsigned char *in, int inlen, char *out) {
|
|
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+1 < inlen) ? b64tab[(n >> 6) & 63] : '=';
|
|
out[j++] = (i+2 < inlen) ? b64tab[n & 63] : '=';
|
|
}
|
|
out[j] = '\0';
|
|
}
|
|
|
|
/* ── IRC line handler (net thread — uses si to tag every event) ────────────── */
|
|
|
|
static void handle_irc_line(int si, const char *raw) {
|
|
IrcMsg m; parse_irc(raw, &m);
|
|
char nick[MAX_NICK]=""; nick_from_prefix(m.prefix, nick, sizeof(nick));
|
|
Server *s = &g_srv[si];
|
|
|
|
if (strcmp(m.cmd,"PING")==0) {
|
|
srv_sendf(si, "PONG :%s", m.trail[0]?m.trail:m.params[0]);
|
|
return;
|
|
}
|
|
|
|
/* SASL / CAP negotiation */
|
|
if (strcmp(m.cmd,"CAP")==0) {
|
|
char sub[16]="";
|
|
if (m.nparams>=2) str_upper(sub, m.params[1], sizeof(sub));
|
|
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 -- 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) {
|
|
/* 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 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) {
|
|
ev_simple(EV_STATUS, si, NULL, NULL, "SASL authentication successful.");
|
|
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,"902")==0||strcmp(m.cmd,"904")==0||
|
|
strcmp(m.cmd,"905")==0||strcmp(m.cmd,"906")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"SASL failed (%s): %s",m.cmd,m.trail);
|
|
ev_simple(EV_ERROR, si, NULL, NULL, buf);
|
|
srv_send_raw(si, "CAP END");
|
|
srv_sendf(si, "NICK %s", s->nick);
|
|
srv_sendf(si, "USER %s 0 * :sirc", s->nick);
|
|
return;
|
|
}
|
|
|
|
/* 001 */
|
|
if (strcmp(m.cmd,"001")==0) {
|
|
s->connected=1;
|
|
ev_simple(EV_CONNECTED, si, NULL, NULL, m.trail);
|
|
ev_simple(EV_SERVER_TEXT, si, NULL, NULL, m.trail);
|
|
return;
|
|
}
|
|
|
|
/* PRIVMSG */
|
|
if (strcmp(m.cmd,"PRIVMSG")==0) {
|
|
Event ev; memset(&ev,0,sizeof(ev));
|
|
ev.type=EV_PRIVMSG; ev.srv=si;
|
|
strncpy(ev.nick, nick, MAX_NICK-1);
|
|
strncpy(ev.chan, m.nparams>0?m.params[0]:"", MAX_CHAN-1);
|
|
strncpy(ev.text, m.trail, MAX_LINE-1);
|
|
ev_push(&ev); return;
|
|
}
|
|
|
|
/* JOIN */
|
|
if (strcmp(m.cmd,"JOIN")==0) {
|
|
const char *ch=m.trail[0]?m.trail:(m.nparams>0?m.params[0]:"");
|
|
ev_simple(EV_JOIN, si, nick, ch, NULL); return;
|
|
}
|
|
|
|
/* PART */
|
|
if (strcmp(m.cmd,"PART")==0) {
|
|
ev_simple(EV_PART, si, nick, m.nparams>0?m.params[0]:"", m.trail);
|
|
return;
|
|
}
|
|
|
|
/* QUIT */
|
|
if (strcmp(m.cmd,"QUIT")==0) {
|
|
ev_simple(EV_QUIT_MSG, si, nick, NULL, m.trail); return;
|
|
}
|
|
|
|
/* NICK */
|
|
if (strcmp(m.cmd,"NICK")==0) {
|
|
Event ev; memset(&ev,0,sizeof(ev));
|
|
ev.type=EV_NICK_CHANGE; ev.srv=si;
|
|
strncpy(ev.nick, nick, MAX_NICK-1);
|
|
const char *nn=m.trail[0]?m.trail:(m.nparams>0?m.params[0]:"");
|
|
strncpy(ev.extra, nn, MAX_NICK-1);
|
|
ev_push(&ev); return;
|
|
}
|
|
|
|
/* 353 NAMES */
|
|
if (strcmp(m.cmd,"353")==0) {
|
|
const char *ch=m.nparams>=3?m.params[2]:"";
|
|
char tmp[MAX_LINE]; strncpy(tmp,m.trail,MAX_LINE-1);
|
|
char *tok=strtok(tmp," ");
|
|
while (tok) {
|
|
Event ev; memset(&ev,0,sizeof(ev));
|
|
ev.type=EV_NAMES; ev.srv=si;
|
|
strncpy(ev.chan, ch, MAX_CHAN-1);
|
|
/* ev.extra[0] = mode prefix char (or 0 if none) */
|
|
const char *n=tok;
|
|
if (*n=='@'||*n=='+'||*n=='~'||*n=='&'||*n=='%') {
|
|
ev.extra[0]=*n; n++;
|
|
}
|
|
strncpy(ev.nick, n, MAX_NICK-1);
|
|
ev_push(&ev);
|
|
tok=strtok(NULL," ");
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* KICK */
|
|
if (strcmp(m.cmd,"KICK")==0) {
|
|
Event ev; memset(&ev,0,sizeof(ev));
|
|
ev.type=EV_KICK; ev.srv=si;
|
|
strncpy(ev.nick, nick, MAX_NICK-1);
|
|
strncpy(ev.chan, m.nparams>0?m.params[0]:"", MAX_CHAN-1);
|
|
strncpy(ev.extra, m.nparams>1?m.params[1]:"", MAX_NICK-1);
|
|
strncpy(ev.text, m.trail, MAX_LINE-1);
|
|
ev_push(&ev); return;
|
|
}
|
|
|
|
/* 433 nick in use */
|
|
if (strcmp(m.cmd,"433")==0) {
|
|
ev_simple(EV_ERROR, si, NULL, NULL, "Nickname already in use.");
|
|
return;
|
|
}
|
|
|
|
/* server text */
|
|
if (strcmp(m.cmd,"002")==0||strcmp(m.cmd,"003")==0||strcmp(m.cmd,"004")==0||
|
|
strcmp(m.cmd,"375")==0||strcmp(m.cmd,"372")==0||strcmp(m.cmd,"376")==0) {
|
|
ev_simple(EV_SERVER_TEXT, si, NULL, NULL,
|
|
m.trail[0]?m.trail:m.params[m.nparams>1?1:0]);
|
|
return;
|
|
}
|
|
|
|
/* MODE */
|
|
if (strcmp(m.cmd,"MODE")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"MODE %s %s",m.params[0],m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
|
|
/* TOPIC */
|
|
if (strcmp(m.cmd,"331")==0) {
|
|
const char *ch=m.nparams>1?m.params[1]:m.params[0];
|
|
char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] No topic set",ch);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"332")==0) {
|
|
const char *ch=m.nparams>1?m.params[1]:m.params[0];
|
|
char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] Topic: %s",ch,m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf);
|
|
ev_simple(EV_TOPIC, si, NULL, ch, m.trail);
|
|
return;
|
|
}
|
|
if (strcmp(m.cmd,"333")==0) {
|
|
const char *ch=m.nparams>1?m.params[1]:"";
|
|
const char *setter=m.nparams>2?m.params[2]:"";
|
|
char sn[MAX_NICK]; strncpy(sn,setter,MAX_NICK-1);
|
|
char *bang=strchr(sn,'!'); if(bang)*bang='\0';
|
|
char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] Topic set by %s",ch,sn);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"TOPIC")==0) {
|
|
const char *ch=m.nparams>0?m.params[0]:"";
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[%s] %s changed topic to: %s",ch,nick,m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf);
|
|
ev_simple(EV_SERVER_TEXT, si, NULL, ch, buf);
|
|
ev_simple(EV_TOPIC, si, NULL, ch, m.trail);
|
|
return;
|
|
}
|
|
|
|
/* WHOIS */
|
|
if (strcmp(m.cmd,"311")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[whois] %s (%s@%s): %s",
|
|
m.nparams>1?m.params[1]:"",m.nparams>2?m.params[2]:"",
|
|
m.nparams>3?m.params[3]:"",m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"312")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[whois] server: %s (%s)",
|
|
m.nparams>2?m.params[2]:"",m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"313")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[whois] %s is an IRC operator",
|
|
m.nparams>1?m.params[1]:"");
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"317")==0) {
|
|
int idle=m.nparams>2?atoi(m.params[2]):0;
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[whois] idle: %dm%ds",idle/60,idle%60);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"318")==0) {
|
|
ev_simple(EV_STATUS, si, NULL, NULL, "[whois] end"); return;
|
|
}
|
|
if (strcmp(m.cmd,"319")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[whois] channels: %s",m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"307")==0||strcmp(m.cmd,"330")==0) {
|
|
char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[whois] %s",m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
|
|
/* WHO */
|
|
if (strcmp(m.cmd,"352")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[who] %-16s %s!%s@%s",
|
|
m.nparams>1?m.params[1]:"*",
|
|
m.nparams>6?m.params[6]:"",
|
|
m.nparams>2?m.params[2]:"",
|
|
m.nparams>3?m.params[3]:"");
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"315")==0) {
|
|
ev_simple(EV_STATUS, si, NULL, NULL, "[who] end"); return;
|
|
}
|
|
|
|
/* LIST */
|
|
if (strcmp(m.cmd,"321")==0) {
|
|
ev_simple(EV_STATUS, si, NULL, NULL, "[list] Channel Users Topic");
|
|
return;
|
|
}
|
|
if (strcmp(m.cmd,"322")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[list] %-20s %-6s %s",
|
|
m.nparams>1?m.params[1]:"",
|
|
m.nparams>2?m.params[2]:"",m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"323")==0) {
|
|
ev_simple(EV_STATUS, si, NULL, NULL, "[list] end"); return;
|
|
}
|
|
|
|
if (strcmp(m.cmd,"366")==0) return;
|
|
|
|
/* AWAY */
|
|
if (strcmp(m.cmd,"301")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"[away] %s is away: %s",
|
|
m.nparams>1?m.params[1]:"",m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"305")==0) {
|
|
ev_simple(EV_STATUS, si, NULL, NULL, "You are no longer marked as away.");
|
|
return;
|
|
}
|
|
if (strcmp(m.cmd,"306")==0) {
|
|
ev_simple(EV_STATUS, si, NULL, NULL, "You have been marked as away.");
|
|
return;
|
|
}
|
|
|
|
/* INVITE */
|
|
if (strcmp(m.cmd,"INVITE")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"** %s invites you to %s",nick,
|
|
m.trail[0]?m.trail:m.params[m.nparams>1?1:0]);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
if (strcmp(m.cmd,"341")==0) {
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"Invited %s to %s",
|
|
m.nparams>1?m.params[1]:"",m.nparams>2?m.params[2]:m.trail);
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf); return;
|
|
}
|
|
|
|
/* NOTICE */
|
|
if (strcmp(m.cmd,"NOTICE")==0) {
|
|
const char *target=m.nparams>0?m.params[0]:"";
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"--%s-- %s",nick[0]?nick:"server",m.trail);
|
|
if (target[0]=='#') {
|
|
Event ev2; memset(&ev2,0,sizeof(ev2));
|
|
ev2.type=EV_SERVER_TEXT; ev2.srv=si;
|
|
strncpy(ev2.chan, target, MAX_CHAN-1);
|
|
strncpy(ev2.text, buf, MAX_LINE-1);
|
|
ev_push(&ev2);
|
|
} else {
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf);
|
|
}
|
|
return;
|
|
}
|
|
|
|
ev_simple(EV_RAW, si, NULL, NULL, raw);
|
|
}
|
|
|
|
/* ── network thread ────────────────────────────────────────────────────────── */
|
|
|
|
static void srv_close(int si) {
|
|
Server *s=&g_srv[si];
|
|
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; }
|
|
s->connected=0;
|
|
}
|
|
|
|
static int srv_connect(int si) {
|
|
Server *s=&g_srv[si];
|
|
char portstr[8]; snprintf(portstr,sizeof(portstr),"%d",s->port);
|
|
struct addrinfo hints,*res,*r;
|
|
memset(&hints,0,sizeof(hints));
|
|
hints.ai_family=AF_UNSPEC; hints.ai_socktype=SOCK_STREAM;
|
|
if (getaddrinfo(s->host,portstr,&hints,&res)!=0) return -1;
|
|
s->sock=-1;
|
|
for (r=res; r; r=r->ai_next) {
|
|
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);
|
|
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);
|
|
struct timeval tv={15,0};
|
|
if (select(fd+1,NULL,&wfds,NULL,&tv)>0) {
|
|
int err=0; socklen_t el=sizeof(err);
|
|
getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&el);
|
|
if (err==0) { s->sock=fd; break; }
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
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 (s->use_tls) {
|
|
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;
|
|
SSL_CTX_set_verify(g_ssl_ctx,SSL_VERIFY_PEER,NULL);
|
|
SSL_CTX_set_default_verify_paths(g_ssl_ctx);
|
|
}
|
|
s->ssl=SSL_new(g_ssl_ctx);
|
|
SSL_set_fd(s->ssl,s->sock);
|
|
SSL_set_tlsext_host_name(s->ssl,s->host);
|
|
if (SSL_connect(s->ssl)<=0) {
|
|
SSL_free(s->ssl); s->ssl=NULL; return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
typedef struct { int si; } NetArg;
|
|
|
|
static void *net_thread(void *arg) {
|
|
NetArg *na=(NetArg*)arg; int si=na->si; free(na);
|
|
Server *s=&g_srv[si];
|
|
|
|
char buf[MAX_LINE];
|
|
snprintf(buf,sizeof(buf),"Connecting to %s:%d%s...",
|
|
s->host,s->port,s->use_tls?" (TLS)":"");
|
|
ev_simple(EV_STATUS, si, NULL, NULL, buf);
|
|
|
|
if (srv_connect(si)<0) {
|
|
ev_simple(EV_ERROR, si, NULL, NULL, "Connection failed.");
|
|
ev_simple(EV_RECONNECT, si, NULL, NULL, NULL);
|
|
return NULL;
|
|
}
|
|
|
|
int sasl_pending=(s->sasl_user[0]&&s->sasl_pass[0])?1:0;
|
|
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",s->nick);
|
|
srv_sendf(si,"USER %s 0 * :sirc",s->nick);
|
|
}
|
|
|
|
char readbuf[8192], linebuf[MAX_LINE*4];
|
|
int linelen=0;
|
|
time_t last_ping=time(NULL);
|
|
|
|
while (!s->net_stop) {
|
|
if (time(NULL)-last_ping>PING_INTERVAL) {
|
|
srv_sendf(si,"PING :%s",s->host);
|
|
last_ping=time(NULL);
|
|
}
|
|
fd_set rfds; FD_ZERO(&rfds); FD_SET(s->sock,&rfds);
|
|
struct timeval tv={5,0};
|
|
int sel=select(s->sock+1,&rfds,NULL,NULL,&tv);
|
|
if (sel==0) continue;
|
|
if (sel<0) { ev_simple(EV_ERROR,si,NULL,NULL,"Select error."); break; }
|
|
|
|
int n;
|
|
if (s->use_tls && s->ssl)
|
|
n=SSL_read(s->ssl,readbuf,sizeof(readbuf)-1);
|
|
else
|
|
n=recv(s->sock,readbuf,sizeof(readbuf)-1,0);
|
|
if (n<=0) { ev_simple(EV_ERROR,si,NULL,NULL,"Server closed connection."); break; }
|
|
readbuf[n]='\0';
|
|
|
|
for (int i=0; i<n; i++) {
|
|
char c=readbuf[i];
|
|
if (c=='\n') {
|
|
if (linelen>0 && linebuf[linelen-1]=='\r') linelen--;
|
|
linebuf[linelen]='\0';
|
|
if (linelen>0) handle_irc_line(si,linebuf);
|
|
linelen=0;
|
|
} else if (linelen<(int)sizeof(linebuf)-1) {
|
|
linebuf[linelen++]=c;
|
|
}
|
|
}
|
|
last_ping=time(NULL);
|
|
}
|
|
|
|
srv_close(si);
|
|
ev_simple(EV_RECONNECT, si, NULL, NULL, NULL);
|
|
return NULL;
|
|
}
|
|
|
|
/* ── reconnect ─────────────────────────────────────────────────────────────── */
|
|
|
|
static void start_net_thread(int si);
|
|
|
|
typedef struct { int si; } ReconArg;
|
|
static void *reconnect_thread(void *arg) {
|
|
ReconArg *ra=(ReconArg*)arg; int si=ra->si; free(ra);
|
|
sleep(RECONNECT_DELAY);
|
|
g_srv[si].reconnect_pending=0;
|
|
start_net_thread(si);
|
|
return NULL;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
static void schedule_reconnect(int si) {
|
|
if (g_srv[si].reconnect_pending) return;
|
|
g_srv[si].reconnect_pending=1;
|
|
srv_status_fmt(si,"Reconnecting in %ds...",RECONNECT_DELAY);
|
|
ReconArg *ra=malloc(sizeof(ReconArg)); ra->si=si;
|
|
pthread_create(&g_srv[si].reconnect_tid, NULL, reconnect_thread, ra);
|
|
pthread_detach(g_srv[si].reconnect_tid);
|
|
}
|
|
|
|
/* ── config file ───────────────────────────────────────────────────────────── */
|
|
|
|
static int cfg_bool(const char *v) {
|
|
return strcasecmp(v,"true")==0||strcasecmp(v,"yes")==0||
|
|
strcasecmp(v,"1")==0||strcasecmp(v,"on")==0;
|
|
}
|
|
|
|
/* add a server entry with defaults; returns its index */
|
|
static int srv_alloc(void) {
|
|
if (g_srv_count>=MAX_SERVERS) return -1;
|
|
int si=g_srv_count++;
|
|
Server *s=&g_srv[si];
|
|
memset(s,0,sizeof(Server));
|
|
strncpy(s->host,"irc.libera.chat",MAX_HOST-1);
|
|
s->port=6697;
|
|
strncpy(s->nick,"sirc_user",MAX_NICK-1);
|
|
s->use_tls=1;
|
|
s->sock=-1;
|
|
pthread_mutex_init(&s->send_lock,NULL);
|
|
return si;
|
|
}
|
|
|
|
static void srv_add_autojoin(int si, const char *chanlist) {
|
|
char tmp[512]; strncpy(tmp,chanlist,511);
|
|
char *tok=strtok(tmp,",");
|
|
while (tok) {
|
|
while (*tok==' ') tok++;
|
|
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]='#'; }
|
|
strncpy(g_srv[si].autojoin[g_srv[si].autojoin_count++],ch,MAX_CHAN-1);
|
|
}
|
|
tok=strtok(NULL,",");
|
|
}
|
|
}
|
|
|
|
static void load_config(const char *path) {
|
|
char candidates[2][MAX_HOST];
|
|
const char *home=getenv("HOME");
|
|
if (home) {
|
|
snprintf(candidates[0],MAX_HOST,"%s/.sirc",home);
|
|
snprintf(candidates[1],MAX_HOST,"%s/.config/sirc/config",home);
|
|
}
|
|
FILE *f=NULL;
|
|
if (path) f=fopen(path,"r");
|
|
else for (int i=0;i<2&&!f;i++) f=fopen(candidates[i],"r");
|
|
if (!f) return;
|
|
|
|
/* defaults that apply before any [server] block */
|
|
char def_nick[MAX_NICK]="sirc_user";
|
|
|
|
/* current server being parsed; -1 = not inside a [server] block */
|
|
int cur_si=-1;
|
|
|
|
char line[512];
|
|
while (fgets(line,sizeof(line),f)) {
|
|
line[strcspn(line,"\r\n")]='\0';
|
|
char *p=line;
|
|
while (*p==' '||*p=='\t') p++;
|
|
if (!*p||*p=='#') continue;
|
|
|
|
/* [server] section header */
|
|
if (strcmp(p,"[server]")==0) {
|
|
cur_si=srv_alloc();
|
|
if (cur_si>=0) strncpy(g_srv[cur_si].nick,def_nick,MAX_NICK-1);
|
|
continue;
|
|
}
|
|
|
|
char *eq=strchr(p,'='); if (!eq) continue;
|
|
*eq='\0';
|
|
char *key=p, *val=eq+1;
|
|
char *ke=key+strlen(key)-1;
|
|
while (ke>key&&(*ke==' '||*ke=='\t')) *ke--='\0';
|
|
while (*val==' '||*val=='\t') val++;
|
|
char *ve=val+strlen(val)-1;
|
|
while (ve>val&&(*ve==' '||*ve=='\t')) *ve--='\0';
|
|
|
|
if (cur_si<0) {
|
|
/* global defaults */
|
|
if (strcmp(key,"nick")==0) strncpy(def_nick,val,MAX_NICK-1);
|
|
else if (strcmp(key,"ignore")==0) {
|
|
char tmp[512]; strncpy(tmp,val,511);
|
|
char *tok=strtok(tmp,",");
|
|
while (tok) { while(*tok==' ')tok++; if(*tok)ignore_add(tok); tok=strtok(NULL,","); }
|
|
}
|
|
} else {
|
|
Server *s=&g_srv[cur_si];
|
|
if (strcmp(key,"host")==0) strncpy(s->host,val,MAX_HOST-1);
|
|
else if (strcmp(key,"port")==0) s->port=atoi(val);
|
|
else if (strcmp(key,"nick")==0) strncpy(s->nick,val,MAX_NICK-1);
|
|
else if (strcmp(key,"tls")==0) s->use_tls=cfg_bool(val);
|
|
else if (strcmp(key,"sasl_user")==0) strncpy(s->sasl_user,val,MAX_NICK-1);
|
|
else if (strcmp(key,"sasl_pass")==0) strncpy(s->sasl_pass,val,255);
|
|
else if (strcmp(key,"channel")==0) srv_add_autojoin(cur_si,val);
|
|
}
|
|
}
|
|
fclose(f);
|
|
}
|
|
|
|
/* ── URL detection ─────────────────────────────────────────────────────────── */
|
|
|
|
static int find_url(const char *str, int start, int *len) {
|
|
for (int i=start; str[i]; i++) {
|
|
if (strncmp(str+i,"http://",7)==0||strncmp(str+i,"https://",8)==0||
|
|
strncmp(str+i,"www.",4)==0) {
|
|
int j=i;
|
|
while (str[j]&&str[j]!=' '&&str[j]!='\t'&&
|
|
str[j]!='"'&&str[j]!='\''&&str[j]!='<') j++;
|
|
*len=j-i; return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* ── draw routines ─────────────────────────────────────────────────────────── */
|
|
|
|
static void draw_status_bar(void) {
|
|
int H,W; getmaxyx(stdscr,H,W); (void)H;
|
|
werase(win_status);
|
|
wbkgd(win_status,COLOR_PAIR(C_HEADER));
|
|
Channel *ch=&g_chans[g_active];
|
|
/* topic starts after the channel panel, truncated on the right */
|
|
int avail = W - CHAN_W - 1;
|
|
if (avail < 1) avail = 1;
|
|
char row[512];
|
|
if (ch->topic[0])
|
|
snprintf(row, sizeof(row), " %.*s", avail-1, ch->topic);
|
|
else
|
|
row[0]='\0';
|
|
int l=strlen(row);
|
|
while (l < avail) row[l++]=' ';
|
|
row[avail]='\0';
|
|
mvwaddnstr(win_status, 0, CHAN_W, row, avail);
|
|
wnoutrefresh(win_status);
|
|
}
|
|
|
|
static void draw_channels(void) {
|
|
int H,W; getmaxyx(win_chan,H,W); (void)W;
|
|
werase(win_chan);
|
|
wattron(win_chan,COLOR_PAIR(C_BORDER)|A_BOLD);
|
|
mvwaddnstr(win_chan,0,0," CHANNELS",CHAN_W-1);
|
|
wattroff(win_chan,COLOR_PAIR(C_BORDER)|A_BOLD);
|
|
|
|
int row=2;
|
|
for (int si=0; si<g_srv_count && row<H-1; si++) {
|
|
/* server header: full hostname, ~ prefix if disconnected */
|
|
char hdr[CHAN_W];
|
|
const char *conn_mark = g_srv[si].connected ? "" : "~";
|
|
snprintf(hdr,sizeof(hdr),"%s%.*s",conn_mark,(int)(CHAN_W-2),g_srv[si].host);
|
|
int hl=strlen(hdr);
|
|
while (hl<CHAN_W-1) hdr[hl++]=' '; hdr[CHAN_W-1]='\0';
|
|
wattron(win_chan,COLOR_PAIR(C_SERVER_HDR)|A_BOLD);
|
|
mvwaddnstr(win_chan,row,0,hdr,CHAN_W-1);
|
|
wattroff(win_chan,COLOR_PAIR(C_SERVER_HDR)|A_BOLD);
|
|
row++;
|
|
|
|
/* show *status* first, then regular channels */
|
|
for (int pass=0; pass<2 && row<H-1; pass++) {
|
|
for (int ci=0; ci<g_chan_count && row<H-1; ci++) {
|
|
if (g_chans[ci].srv!=si) continue;
|
|
int is_status=(strcmp(g_chans[ci].name,"*status*")==0);
|
|
if (pass==0 && !is_status) continue; /* pass 0: status only */
|
|
if (pass==1 && is_status) continue; /* pass 1: channels only */
|
|
Channel *ch=&g_chans[ci];
|
|
int attr; const char *pfx;
|
|
if (ci==g_active) {
|
|
attr=COLOR_PAIR(C_CHAN_SEL)|A_BOLD; pfx="> ";
|
|
} else if (is_status) {
|
|
attr=COLOR_PAIR(C_STATUS)|A_DIM; pfx=" ";
|
|
} else if (ch->mention) {
|
|
attr=COLOR_PAIR(C_MENTION_CHAN)|A_BOLD; pfx="! ";
|
|
} else if (ch->unread) {
|
|
attr=COLOR_PAIR(C_UNREAD); pfx="+ ";
|
|
} else {
|
|
attr=COLOR_PAIR(C_CHAN); pfx=" ";
|
|
}
|
|
char label[CHAN_W+3];
|
|
snprintf(label,sizeof(label)," %s%.*s",pfx,CHAN_W-4,ch->name);
|
|
int ll=strlen(label);
|
|
while (ll<CHAN_W-1) label[ll++]=' '; label[CHAN_W-1]='\0';
|
|
wattron(win_chan,attr);
|
|
mvwaddnstr(win_chan,row,0,label,CHAN_W-1);
|
|
wattroff(win_chan,attr);
|
|
row++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* bottom: active channel name */
|
|
char hint[CHAN_W];
|
|
snprintf(hint,sizeof(hint),"%.*s",CHAN_W-1,g_chans[g_active].name);
|
|
int hl=strlen(hint);
|
|
while (hl<CHAN_W-1) hint[hl++]=' '; hint[CHAN_W-1]='\0';
|
|
wattron(win_chan,COLOR_PAIR(C_STATUS)|A_DIM);
|
|
mvwaddnstr(win_chan,H-1,0,hint,CHAN_W-1);
|
|
wattroff(win_chan,COLOR_PAIR(C_STATUS)|A_DIM);
|
|
|
|
wnoutrefresh(win_chan);
|
|
}
|
|
|
|
static void draw_chat(void) {
|
|
int chat_h,chat_w; getmaxyx(win_chat,chat_h,chat_w);
|
|
werase(win_chat);
|
|
Channel *ch=&g_chans[g_active];
|
|
if (ch->line_count==0) { wnoutrefresh(win_chat); return; }
|
|
|
|
typedef struct { int flag; char text[MAX_LINE]; int has_url; } FlatLine;
|
|
static FlatLine flat[MAX_HISTORY*4];
|
|
int flat_count=0;
|
|
|
|
for (int li=0; li<ch->line_count && flat_count<(int)(sizeof(flat)/sizeof(flat[0])); li++) {
|
|
Line *ln=chan_line(ch,li); if (!ln) continue;
|
|
char full[MAX_LINE];
|
|
snprintf(full,sizeof(full),"%s %s",ln->ts,ln->text);
|
|
int plen=strlen(ln->ts)+1;
|
|
int avail=chat_w-plen; if(avail<4)avail=4;
|
|
const char *p=full; int is_first=1;
|
|
while (*p && flat_count<(int)(sizeof(flat)/sizeof(flat[0]))) {
|
|
int w=is_first?chat_w:avail;
|
|
FlatLine *fl=&flat[flat_count++];
|
|
fl->flag=ln->flag;
|
|
fl->has_url=(find_url(ln->text,0,&(int){0})>=0);
|
|
if ((int)strlen(p)<=w) {
|
|
if (!is_first) { memset(fl->text,' ',plen); strncpy(fl->text+plen,p,MAX_LINE-plen-1); }
|
|
else strncpy(fl->text,p,MAX_LINE-1);
|
|
break;
|
|
}
|
|
int cut=w;
|
|
while (cut>0&&p[cut]!=' ') cut--;
|
|
if (cut==0) cut=w;
|
|
if (!is_first) { memset(fl->text,' ',plen); memcpy(fl->text+plen,p,cut); fl->text[plen+cut]='\0'; }
|
|
else { memcpy(fl->text,p,cut); fl->text[cut]='\0'; }
|
|
is_first=0; p+=cut; while(*p==' ')p++;
|
|
}
|
|
}
|
|
|
|
int max_scroll=flat_count>chat_h?flat_count-chat_h:0;
|
|
if (ch->scroll>max_scroll) ch->scroll=max_scroll;
|
|
if (ch->scroll<0) ch->scroll=0;
|
|
int vis_start=flat_count-chat_h-ch->scroll;
|
|
if (vis_start<0) vis_start=0;
|
|
int vis_end=vis_start+chat_h;
|
|
if (vis_end>flat_count) vis_end=flat_count;
|
|
|
|
for (int i=vis_start; i<vis_end; i++) {
|
|
int row=i-vis_start;
|
|
FlatLine *fl=&flat[i];
|
|
if (fl->flag==F_MSG) {
|
|
wattron(win_chat,COLOR_PAIR(C_STATUS));
|
|
mvwaddnstr(win_chat,row,0,fl->text,chat_w-1);
|
|
wattroff(win_chat,COLOR_PAIR(C_STATUS));
|
|
const char *lt=strchr(fl->text,'<');
|
|
const char *gt=lt?strchr(lt,'>'):NULL;
|
|
if (lt&>) {
|
|
int ns=(int)(lt-fl->text), nl=(int)(gt-lt+1);
|
|
char nn[MAX_NICK]; int nlen=nl-2; if(nlen>=MAX_NICK)nlen=MAX_NICK-1;
|
|
memcpy(nn,lt+1,nlen); nn[nlen]='\0';
|
|
mvwchgat(win_chat,row,ns,nl,A_BOLD,nick_colour(nn),NULL);
|
|
}
|
|
} else {
|
|
int attr;
|
|
switch(fl->flag) {
|
|
case F_ME: attr=COLOR_PAIR(C_ME)|A_BOLD; break;
|
|
case F_MENTION: attr=COLOR_PAIR(C_MENTION)|A_BOLD; break;
|
|
case F_JOIN: attr=COLOR_PAIR(C_JOIN); break;
|
|
case F_ACTION: attr=COLOR_PAIR(C_ACTION)|A_ITALIC; break;
|
|
default: attr=COLOR_PAIR(C_STATUS); break;
|
|
}
|
|
wattron(win_chat,attr);
|
|
mvwaddnstr(win_chat,row,0,fl->text,chat_w-1);
|
|
wattroff(win_chat,attr);
|
|
}
|
|
if (fl->has_url) {
|
|
int ustart=0,ulen,us;
|
|
while ((us=find_url(fl->text,ustart,&ulen))>=0) {
|
|
if (us>=chat_w-1) break;
|
|
int ue=us+ulen; if(ue>chat_w-2)ue=chat_w-2;
|
|
mvwchgat(win_chat,row,us,ue-us,A_UNDERLINE,C_URL,NULL);
|
|
ustart=us+ulen;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ch->scroll>0) {
|
|
char tag[16]; snprintf(tag,sizeof(tag),"^%d",ch->scroll);
|
|
wattron(win_chat,COLOR_PAIR(C_STATUS)|A_DIM);
|
|
mvwaddstr(win_chat,0,chat_w-(int)strlen(tag)-1,tag);
|
|
wattroff(win_chat,COLOR_PAIR(C_STATUS)|A_DIM);
|
|
}
|
|
wnoutrefresh(win_chat);
|
|
}
|
|
|
|
static void draw_users(void) {
|
|
int H,W; getmaxyx(win_users,H,W); (void)W;
|
|
werase(win_users);
|
|
Channel *ch=&g_chans[g_active];
|
|
char hdr[32]; snprintf(hdr,sizeof(hdr)," USERS (%d)",ch->user_count);
|
|
wattron(win_users,COLOR_PAIR(C_BORDER)|A_BOLD);
|
|
mvwaddnstr(win_users,0,1,hdr,USER_W-2);
|
|
wattroff(win_users,COLOR_PAIR(C_BORDER)|A_BOLD);
|
|
for (int i=0; i<ch->user_count; i++) {
|
|
int y=i+2; if(y>=H-1) break;
|
|
const char *entry = ch->users[i];
|
|
char mode = entry[0];
|
|
const char *bare = entry+1; /* nick without prefix */
|
|
/* colour the mode symbol */
|
|
if (mode=='@'||mode=='~'||mode=='&') {
|
|
wattron(win_users, COLOR_PAIR(C_MENTION)|A_BOLD);
|
|
mvwaddnstr(win_users,y,1,&mode,1);
|
|
wattroff(win_users, COLOR_PAIR(C_MENTION)|A_BOLD);
|
|
} else if (mode=='+'||mode=='%') {
|
|
wattron(win_users, COLOR_PAIR(C_ME)|A_BOLD);
|
|
mvwaddnstr(win_users,y,1,&mode,1);
|
|
wattroff(win_users, COLOR_PAIR(C_ME)|A_BOLD);
|
|
} else {
|
|
mvwaddch(win_users,y,1,' ');
|
|
}
|
|
/* nick in normal white */
|
|
wattron(win_users,COLOR_PAIR(C_CHAN));
|
|
mvwaddnstr(win_users,y,2,bare,USER_W-3);
|
|
wattroff(win_users,COLOR_PAIR(C_CHAN));
|
|
}
|
|
wnoutrefresh(win_users);
|
|
}
|
|
|
|
static void draw_input(void) {
|
|
int iw; getmaxyx(win_input,(int){0},iw);
|
|
werase(win_input);
|
|
wbkgd(win_input,COLOR_PAIR(C_INPUT));
|
|
|
|
/* draw "> " prompt */
|
|
wattron(win_input, COLOR_PAIR(C_BORDER)|A_BOLD);
|
|
mvwaddstr(win_input, 0, 0, "> ");
|
|
wattroff(win_input, COLOR_PAIR(C_BORDER)|A_BOLD);
|
|
|
|
/* available width after prompt */
|
|
int pfx = 2;
|
|
int avail = iw - pfx - 1;
|
|
if (avail < 1) avail = 1;
|
|
|
|
/* scroll text so cursor stays visible */
|
|
int disp = 0, cx = g_input_cur;
|
|
if (g_input_cur > avail) {
|
|
disp = g_input_cur - avail;
|
|
cx = avail;
|
|
}
|
|
|
|
wattron(win_input, COLOR_PAIR(C_INPUT));
|
|
mvwaddnstr(win_input, 0, pfx, g_input + disp, avail);
|
|
wattroff(win_input, COLOR_PAIR(C_INPUT));
|
|
|
|
/* place terminal cursor — curs_set(2) gives blinking block on most terms */
|
|
wmove(win_input, 0, pfx + cx);
|
|
wnoutrefresh(win_input);
|
|
}
|
|
|
|
static void draw_all(void) {
|
|
draw_status_bar(); draw_channels(); draw_chat();
|
|
draw_users(); draw_input(); doupdate();
|
|
}
|
|
|
|
/* ── window setup ──────────────────────────────────────────────────────────── */
|
|
|
|
static void init_colors(void) {
|
|
start_color(); use_default_colors();
|
|
init_pair(C_BORDER, COLOR_CYAN, -1);
|
|
init_pair(C_HEADER, COLOR_BLACK, COLOR_CYAN);
|
|
init_pair(C_ME, COLOR_YELLOW, -1);
|
|
init_pair(C_JOIN, COLOR_BLACK+8, -1);
|
|
init_pair(C_MENTION, COLOR_RED, -1);
|
|
init_pair(C_STATUS, COLOR_WHITE, -1);
|
|
init_pair(C_INPUT, COLOR_WHITE, COLOR_BLACK);
|
|
init_pair(C_CHAN_SEL, COLOR_BLACK, COLOR_WHITE);
|
|
init_pair(C_CHAN, COLOR_WHITE, -1);
|
|
init_pair(C_UNREAD, COLOR_YELLOW, -1);
|
|
init_pair(C_MENTION_CHAN,COLOR_RED, -1);
|
|
init_pair(C_URL, COLOR_BLUE, -1);
|
|
init_pair(C_ACTION, COLOR_MAGENTA, -1);
|
|
init_pair(C_SERVER_HDR, COLOR_CYAN, -1);
|
|
static const int nick_cols[C_NICK_COUNT]={
|
|
COLOR_GREEN,COLOR_CYAN,COLOR_MAGENTA,COLOR_YELLOW,
|
|
COLOR_WHITE,COLOR_RED, COLOR_BLUE, COLOR_GREEN
|
|
};
|
|
for (int i=0;i<C_NICK_COUNT;i++)
|
|
init_pair(C_NICK_BASE+i,nick_cols[i],-1);
|
|
}
|
|
|
|
static void build_windows(void) {
|
|
int H,W; getmaxyx(stdscr,H,W);
|
|
int chat_w=W-CHAN_W-USER_W, chat_h=H-3;
|
|
if(win_chan) delwin(win_chan);
|
|
if(win_chat) delwin(win_chat);
|
|
if(win_users) delwin(win_users);
|
|
if(win_input) delwin(win_input);
|
|
if(win_status) delwin(win_status);
|
|
win_status=newwin(1, W, 0, 0);
|
|
win_chan =newwin(H, CHAN_W, 0, 0);
|
|
win_chat =newwin(chat_h,chat_w,1, CHAN_W);
|
|
win_users =newwin(H, USER_W,0, CHAN_W+chat_w);
|
|
win_input =newwin(1, chat_w,H-1,CHAN_W);
|
|
}
|
|
|
|
/* ── 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) {
|
|
for (int j=0;j<g_srv[si].autojoin_count;j++)
|
|
srv_sendf(si,"JOIN %s",g_srv[si].autojoin[j]);
|
|
}
|
|
}
|
|
|
|
static void handle_event(const Event *ev) {
|
|
int si=ev->srv;
|
|
Server *s=&g_srv[si];
|
|
|
|
switch (ev->type) {
|
|
|
|
case EV_CONNECTED:
|
|
srv_status_msg(si,"Connected.");
|
|
do_rejoin_channels(si);
|
|
break;
|
|
|
|
case EV_STATUS:
|
|
srv_status_msg(si,ev->text);
|
|
break;
|
|
|
|
case EV_ERROR:
|
|
srv_status_fmt(si,"[ERR] %s",ev->text);
|
|
break;
|
|
|
|
case EV_RECONNECT:
|
|
if (!s->connected) schedule_reconnect(si);
|
|
break;
|
|
|
|
case EV_SERVER_TEXT:
|
|
if (ev->chan[0]) {
|
|
int ci=chan_find(si,ev->chan);
|
|
if (ci<0) ci=chan_add(si,ev->chan);
|
|
chan_addline(&g_chans[ci],F_STATUS,ev->text);
|
|
if (ci==g_active) g_chans[ci].unread=0;
|
|
} else {
|
|
srv_status_msg(si,ev->text);
|
|
}
|
|
break;
|
|
|
|
case EV_RAW:
|
|
srv_status_msg(si,ev->text);
|
|
break;
|
|
|
|
case EV_PRIVMSG: {
|
|
if (is_ignored(ev->nick)) break;
|
|
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 (text[0]=='\x01'&&strncmp(text+1,"ACTION",6)==0) {
|
|
const char *act=text+8;
|
|
char msg[MAX_LINE];
|
|
int alen=strlen(act); if(alen>0&&act[alen-1]=='\x01')alen--;
|
|
snprintf(msg,sizeof(msg),"* %s %.*s",ev->nick,alen,act);
|
|
int flag=(strcasestr(msg,s->nick)!=NULL)?F_MENTION:F_ACTION;
|
|
chan_addline(&g_chans[ci],flag,msg);
|
|
if(ci!=g_active&&flag==F_MENTION) g_chans[ci].mention=1;
|
|
if(ci==g_active) g_chans[ci].unread=0;
|
|
break;
|
|
}
|
|
char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<%s> %s",ev->nick,text);
|
|
int flag;
|
|
if (strcmp(ev->nick,s->nick)==0) flag=F_ME;
|
|
else if (strcasestr(text,s->nick)!=NULL) flag=F_MENTION;
|
|
else flag=F_MSG;
|
|
chan_addline(&g_chans[ci],flag,msg);
|
|
if(ci!=g_active&&flag==F_MENTION) g_chans[ci].mention=1;
|
|
if(ci==g_active) g_chans[ci].unread=0;
|
|
break;
|
|
}
|
|
|
|
case EV_JOIN: {
|
|
int ci=chan_find(si,ev->chan); if(ci<0) ci=chan_add(si,ev->chan);
|
|
if (strcmp(ev->nick,s->nick)==0) {
|
|
g_chans[ci].user_count=0;
|
|
g_active=ci;
|
|
g_chans[ci].unread=0; g_chans[ci].mention=0;
|
|
srv_status_fmt(si,"Joined %s",ev->chan);
|
|
} else {
|
|
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);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EV_PART: {
|
|
int ci=chan_find(si,ev->chan); if(ci<0) break;
|
|
char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<- %s parted (%s)",ev->nick,ev->text);
|
|
chan_addline(&g_chans[ci],F_JOIN,msg);
|
|
if (strcmp(ev->nick,s->nick)==0) chan_remove(ci);
|
|
else chan_removeuser(&g_chans[ci],ev->nick);
|
|
break;
|
|
}
|
|
|
|
case EV_QUIT_MSG:
|
|
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;}
|
|
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);
|
|
chan_removeuser(&g_chans[i],ev->nick);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EV_NICK_CHANGE:
|
|
if (strcmp(ev->nick,s->nick)==0) {
|
|
strncpy(s->nick,ev->extra,MAX_NICK-1);
|
|
srv_status_fmt(si,"You are now known as %s",s->nick);
|
|
}
|
|
for (int i=0;i<g_chan_count;i++) {
|
|
if (g_chans[i].srv!=si) continue;
|
|
for (int j=0;j<g_chans[i].user_count;j++) {
|
|
/* skip stored mode prefix when comparing */
|
|
const char *sn=g_chans[i].users[j];
|
|
if (*sn=='@'||*sn=='+'||*sn=='~'||*sn=='&'||*sn=='%'||*sn==' ') sn++;
|
|
if (strcasecmp(sn,ev->nick)==0) {
|
|
char mode=g_chans[i].users[j][0];
|
|
g_chans[i].users[j][0]=mode;
|
|
strncpy(g_chans[i].users[j]+1,ev->extra,MAX_NICK-2);
|
|
char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"~ %s is now %s",ev->nick,ev->extra);
|
|
chan_addline(&g_chans[i],F_JOIN,msg);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EV_NAMES: {
|
|
int ci=chan_find(si,ev->chan); if(ci<0) break;
|
|
chan_adduser(&g_chans[ci], ev->extra[0], ev->nick);
|
|
chan_sort_users(&g_chans[ci]);
|
|
break;
|
|
}
|
|
|
|
case EV_KICK: {
|
|
int ci=chan_find(si,ev->chan); if(ci<0) break;
|
|
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);
|
|
break;
|
|
}
|
|
|
|
case EV_TOPIC: {
|
|
int ci=chan_find(si,ev->chan); if(ci<0) break;
|
|
strip_irc_fmt(ev->text, g_chans[ci].topic, MAX_LINE);
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/* ── tab completion ────────────────────────────────────────────────────────── */
|
|
|
|
static void tab_reset(void) { g_tab_active=0; g_tab_count=0; g_tab_idx=0; }
|
|
|
|
static void tab_complete(void) {
|
|
if (g_active<0||g_active>=g_chan_count) return;
|
|
Channel *ch=&g_chans[g_active];
|
|
if (!g_tab_active) {
|
|
int ws=g_input_cur;
|
|
while (ws>0&&g_input[ws-1]!=' '&&g_input[ws-1]!='\t') ws--;
|
|
char prefix[MAX_NICK]; int plen=g_input_cur-ws;
|
|
if (plen<=0) return;
|
|
strncpy(prefix,g_input+ws,plen); prefix[plen]='\0';
|
|
char *pp=prefix;
|
|
while(*pp=='@'||*pp=='+'||*pp=='~'||*pp=='&'||*pp=='%') pp++;
|
|
if (!*pp) return;
|
|
g_tab_count=0;
|
|
for (int i=0;i<ch->user_count;i++) {
|
|
const char *bare=ch->users[i]+1; /* skip mode prefix */
|
|
if (str_icase_starts(bare,pp))
|
|
strncpy(g_tab_matches[g_tab_count++],bare,MAX_NICK-1);
|
|
}
|
|
if (!g_tab_count) return;
|
|
g_tab_idx=0; g_tab_word_start=ws; g_tab_active=1;
|
|
} else {
|
|
g_tab_idx=(g_tab_idx+1)%g_tab_count;
|
|
}
|
|
const char *nick=g_tab_matches[g_tab_idx];
|
|
char repl[MAX_NICK+3];
|
|
snprintf(repl,sizeof(repl),g_tab_word_start==0?"%s: ":"%s ",nick);
|
|
int rlen=strlen(repl);
|
|
char newbuf[MAX_INPUT];
|
|
int tail=g_input_len-g_input_cur; if(tail<0)tail=0;
|
|
memcpy(newbuf,g_input,g_tab_word_start);
|
|
memcpy(newbuf+g_tab_word_start,repl,rlen);
|
|
memcpy(newbuf+g_tab_word_start+rlen,g_input+g_input_cur,tail);
|
|
int newlen=g_tab_word_start+rlen+tail;
|
|
if(newlen>=MAX_INPUT)newlen=MAX_INPUT-1;
|
|
memcpy(g_input,newbuf,newlen); g_input[newlen]='\0';
|
|
g_input_len=newlen; g_input_cur=g_tab_word_start+rlen;
|
|
}
|
|
|
|
/* ── command processing ────────────────────────────────────────────────────── */
|
|
|
|
/* current server index = server of the active channel */
|
|
static int active_srv(void) {
|
|
if (g_active>=0&&g_active<g_chan_count) return g_chans[g_active].srv;
|
|
return 0;
|
|
}
|
|
|
|
static void process_input(const char *text) {
|
|
int si=active_srv();
|
|
Server *s=&g_srv[si];
|
|
|
|
if (text[0]!='/') {
|
|
if (g_active<0) { srv_status_msg(si,"Use /join #channel first."); return; }
|
|
const char *cn=g_chans[g_active].name;
|
|
if (cn[0]=='*') { srv_status_msg(si,"Use /join #channel first."); return; }
|
|
if (!s->connected) { srv_status_msg(si,"[not connected]"); return; }
|
|
srv_sendf(si,"PRIVMSG %s :%s",cn,text);
|
|
char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<%s> %s",s->nick,text);
|
|
chan_addline(&g_chans[g_active],F_ME,msg);
|
|
g_chans[g_active].unread=0;
|
|
return;
|
|
}
|
|
|
|
char cmd[64],arg[MAX_INPUT];
|
|
cmd[0]='\0'; arg[0]='\0';
|
|
const char *sp=strchr(text+1,' ');
|
|
if (sp) {
|
|
int cl=(int)(sp-text); if(cl>=(int)sizeof(cmd))cl=sizeof(cmd)-1;
|
|
memcpy(cmd,text,cl); cmd[cl]='\0';
|
|
const char *a=sp+1; while(*a==' ')a++;
|
|
strncpy(arg,a,MAX_INPUT-1);
|
|
} else strncpy(cmd,text,sizeof(cmd)-1);
|
|
for (char *c=cmd;*c;c++) *c=tolower((unsigned char)*c);
|
|
|
|
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(s->connected) srv_sendf(si,"JOIN %s",ch);
|
|
else srv_status_msg(si,"[not connected]");
|
|
|
|
} else if (strcmp(cmd,"/part")==0) {
|
|
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) {
|
|
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;}
|
|
srv_sendf(si,"PART %s :cycling",ch); srv_sendf(si,"JOIN %s",ch);
|
|
|
|
} else if (strcmp(cmd,"/nick")==0) {
|
|
if(!arg[0]) {
|
|
srv_status_fmt(si,"Your nick is: %s",s->nick);
|
|
} else {
|
|
if(s->connected) srv_sendf(si,"NICK %s",arg);
|
|
else strncpy(s->nick,arg,MAX_NICK-1);
|
|
srv_status_fmt(si,"Nick change requested: %s -> %s",s->nick,arg);
|
|
}
|
|
|
|
} 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);
|
|
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); }
|
|
}
|
|
|
|
} else if (strcmp(cmd,"/me")==0) {
|
|
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;}
|
|
srv_sendf(si,"PRIVMSG %s :\x01""ACTION %s\x01",cn,arg);
|
|
char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"* %s %s",s->nick,arg);
|
|
chan_addline(&g_chans[g_active],F_ACTION,msg);
|
|
|
|
} else if (strcmp(cmd,"/notice")==0) {
|
|
char tgt[MAX_NICK],msg[MAX_LINE];
|
|
if(sscanf(arg,"%63s %[^\n]",tgt,msg)==2) {
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
srv_sendf(si,"NOTICE %s :%s",tgt,msg);
|
|
srv_status_fmt(si,"->%s<- %s",tgt,msg);
|
|
} else srv_status_msg(si,"Usage: /notice <target> <message>");
|
|
|
|
} else if (strcmp(cmd,"/ctcp")==0) {
|
|
char tgt[MAX_NICK],ctcp[MAX_LINE];
|
|
if(sscanf(arg,"%63s %[^\n]",tgt,ctcp)>=1) {
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
for(char *c=ctcp;*c&&*c!=' ';c++) *c=toupper((unsigned char)*c);
|
|
srv_sendf(si,"PRIVMSG %s :\x01%s\x01",tgt,ctcp);
|
|
srv_status_fmt(si,"[ctcp] %s -> %s",ctcp,tgt);
|
|
} else srv_status_msg(si,"Usage: /ctcp <nick> <command>");
|
|
|
|
} else if (strcmp(cmd,"/names")==0) {
|
|
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);
|
|
else {
|
|
int ci=chan_find(si,ch); if(ci<0){srv_status_msg(si,"Channel not found.");return;}
|
|
srv_status_fmt(si,"[names] %s (%d users):",ch,g_chans[ci].user_count);
|
|
char line[MAX_LINE]; line[0]='\0';
|
|
for(int i=0;i<g_chans[ci].user_count;i++){
|
|
if(strlen(line)+strlen(g_chans[ci].users[i])+2>60){
|
|
srv_status_fmt(si,"[names] %s",line); line[0]='\0';
|
|
}
|
|
if(line[0]) strncat(line," ",MAX_LINE-1);
|
|
strncat(line,g_chans[ci].users[i],MAX_LINE-1);
|
|
}
|
|
if(line[0]) srv_status_fmt(si,"[names] %s",line);
|
|
}
|
|
|
|
} else if (strcmp(cmd,"/topic")==0) {
|
|
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;}
|
|
if(arg[0]) srv_sendf(si,"TOPIC %s :%s",ch,arg);
|
|
else srv_sendf(si,"TOPIC %s",ch);
|
|
|
|
} else if (strcmp(cmd,"/whois")==0) {
|
|
if(!arg[0]){srv_status_msg(si,"Usage: /whois <nick>");return;}
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
srv_sendf(si,"WHOIS %s",arg);
|
|
|
|
} else if (strcmp(cmd,"/who")==0) {
|
|
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);
|
|
|
|
} else if (strcmp(cmd,"/list")==0) {
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
srv_status_msg(si,"[list] Requesting channel list...");
|
|
if(arg[0]) srv_sendf(si,"LIST %s",arg); else srv_send_raw(si,"LIST");
|
|
|
|
} else if (strcmp(cmd,"/away")==0) {
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
if(arg[0]) srv_sendf(si,"AWAY :%s",arg); else srv_send_raw(si,"AWAY");
|
|
|
|
} else if (strcmp(cmd,"/back")==0) {
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
srv_send_raw(si,"AWAY");
|
|
|
|
} else if (strcmp(cmd,"/invite")==0) {
|
|
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);
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
srv_sendf(si,"INVITE %s %s",tgt,ch);
|
|
|
|
} else if (strcmp(cmd,"/kick")==0) {
|
|
char tgt[MAX_NICK],reason[MAX_LINE]; reason[0]='\0';
|
|
if(sscanf(arg,"%63s %[^\n]",tgt,reason)<1){srv_status_msg(si,"Usage: /kick <nick> [reason]");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;}
|
|
if(reason[0]) srv_sendf(si,"KICK %s %s :%s",ch,tgt,reason);
|
|
else srv_sendf(si,"KICK %s %s",ch,tgt);
|
|
|
|
} 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);
|
|
else srv_sendf(si,"MODE %s",arg);
|
|
|
|
} else if (strcmp(cmd,"/raw")==0||strcmp(cmd,"/quote")==0) {
|
|
if(!arg[0]){srv_status_msg(si,"Usage: /raw <irc line>");return;}
|
|
if(!s->connected){srv_status_msg(si,"[not connected]");return;}
|
|
srv_send_raw(si,arg);
|
|
srv_status_fmt(si,">> %s",arg);
|
|
|
|
} else if (strcmp(cmd,"/server")==0) {
|
|
/* /server host [port] — add a new server connection */
|
|
char newhost[MAX_HOST]; int newport=6697;
|
|
if(sscanf(arg,"%255s %d",newhost,&newport)<1){srv_status_msg(si,"Usage: /server <host> [port]");return;}
|
|
int nsi=srv_alloc();
|
|
if(nsi<0){srv_status_msg(si,"Max servers reached.");return;}
|
|
strncpy(g_srv[nsi].host,newhost,MAX_HOST-1);
|
|
g_srv[nsi].port=newport;
|
|
/* inherit nick from current server */
|
|
strncpy(g_srv[nsi].nick,s->nick,MAX_NICK-1);
|
|
chan_add(nsi,"*status*");
|
|
start_net_thread(nsi);
|
|
srv_status_fmt(nsi,"Connecting to %s:%d...",newhost,newport);
|
|
|
|
} else if (strcmp(cmd,"/connect")==0) {
|
|
g_srv[si].net_stop=1;
|
|
start_net_thread(si);
|
|
|
|
} else if (strcmp(cmd,"/ignore")==0) {
|
|
if(!arg[0]) {
|
|
if(!g_ignore_count){srv_status_msg(si,"Ignore list is empty.");return;}
|
|
srv_status_msg(si,"Ignored nicks:");
|
|
for(int i=0;i<g_ignore_count;i++) srv_status_fmt(si," %s",g_ignore[i]);
|
|
return;
|
|
}
|
|
ignore_add(arg); srv_status_fmt(si,"Ignoring %s",arg);
|
|
|
|
} else if (strcmp(cmd,"/unignore")==0) {
|
|
if(is_ignored(arg)){ignore_remove(arg);srv_status_fmt(si,"Unignored %s",arg);}
|
|
else srv_status_fmt(si,"%s is not ignored",arg);
|
|
|
|
} else if (strcmp(cmd,"/clear")==0) {
|
|
g_chans[g_active].line_count=0;
|
|
g_chans[g_active].line_head=0;
|
|
g_chans[g_active].scroll=0;
|
|
|
|
} else if (strcmp(cmd,"/quit")==0) {
|
|
for (int i=0;i<g_srv_count;i++)
|
|
if(g_srv[i].connected) srv_sendf(i,"QUIT :%s",arg[0]?arg:"Goodbye");
|
|
endwin(); exit(0);
|
|
|
|
} else if (strcmp(cmd,"/help")==0) {
|
|
static const char *h[]={
|
|
"Commands:",
|
|
" /join #chan join channel",
|
|
" /part [#chan] leave channel",
|
|
" /cycle [#chan] part and rejoin",
|
|
" /nick NAME change nick",
|
|
" /msg NICK TEXT private message",
|
|
" /me TEXT action message",
|
|
" /notice TGT TEXT send notice",
|
|
" /ctcp NICK CMD CTCP request",
|
|
" /names [#chan] list users",
|
|
" /topic [text] get/set topic",
|
|
" /whois NICK whois lookup",
|
|
" /who [target] who query",
|
|
" /list [pattern] list channels",
|
|
" /away [msg] set away",
|
|
" /back clear away",
|
|
" /invite NICK [chan] invite to channel",
|
|
" /kick NICK [reason] kick from channel",
|
|
" /mode [target] [m] get/set mode",
|
|
" /server HOST [port] connect to new server",
|
|
" /raw <line> send raw IRC line",
|
|
" /ignore [nick] ignore (no arg = list)",
|
|
" /unignore NICK unignore nick",
|
|
" /clear clear scrollback",
|
|
" /connect reconnect current server",
|
|
" /quit [msg] quit",
|
|
"Keys:",
|
|
" Tab nick completion",
|
|
" Ctrl+N/P next/prev channel",
|
|
" PgUp/PgDn scroll chat",
|
|
" Ctrl+W delete word left",
|
|
" Up/Down input history",
|
|
NULL
|
|
};
|
|
for(int i=0;h[i];i++) srv_status_msg(si,h[i]);
|
|
|
|
} else {
|
|
srv_status_fmt(si,"Unknown command: %s (/help for list)",cmd);
|
|
}
|
|
}
|
|
|
|
/* ── history ───────────────────────────────────────────────────────────────── */
|
|
|
|
static void hist_push(const char *text) {
|
|
if(g_hist_count<MAX_HIST_LINES) strncpy(g_hist[g_hist_count++].text,text,MAX_INPUT-1);
|
|
else { memmove(g_hist,g_hist+1,(MAX_HIST_LINES-1)*sizeof(HistEntry)); strncpy(g_hist[MAX_HIST_LINES-1].text,text,MAX_INPUT-1); }
|
|
}
|
|
|
|
/* ── key handler ───────────────────────────────────────────────────────────── */
|
|
|
|
static void handle_key(int key) {
|
|
if (key==KEY_RESIZE) { clear(); build_windows(); return; }
|
|
if (key=='\t') { tab_complete(); return; }
|
|
tab_reset();
|
|
|
|
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==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; }
|
|
|
|
if (key=='\n'||key=='\r'||key==KEY_ENTER) {
|
|
if(g_input_len>0) {
|
|
g_input[g_input_len]='\0';
|
|
char *p=g_input; while(*p==' ')p++;
|
|
char *e=g_input+g_input_len-1; while(e>p&&*e==' ')*e--='\0';
|
|
if(*p) { hist_push(p); g_hist_pos=-1; process_input(p); }
|
|
}
|
|
g_input[0]='\0'; g_input_len=0; g_input_cur=0; return;
|
|
}
|
|
|
|
if (key==127||key==KEY_BACKSPACE||key==8) {
|
|
if(g_input_cur>0){ memmove(g_input+g_input_cur-1,g_input+g_input_cur,g_input_len-g_input_cur+1); g_input_cur--; g_input_len--; }
|
|
return;
|
|
}
|
|
|
|
if (key==23) { /* Ctrl+W */
|
|
int cur=g_input_cur;
|
|
while(cur>0&&g_input[cur-1]==' ') cur--;
|
|
while(cur>0&&g_input[cur-1]!=' ') cur--;
|
|
int del=g_input_cur-cur;
|
|
memmove(g_input+cur,g_input+g_input_cur,g_input_len-g_input_cur+1);
|
|
g_input_len-=del; g_input_cur=cur; return;
|
|
}
|
|
|
|
if (key==KEY_DC) { if(g_input_cur<g_input_len){ memmove(g_input+g_input_cur,g_input+g_input_cur+1,g_input_len-g_input_cur); g_input_len--; } return; }
|
|
|
|
if (key==KEY_LEFT &&g_input_cur>0) { g_input_cur--; return; }
|
|
if (key==KEY_RIGHT &&g_input_cur<g_input_len) { g_input_cur++; return; }
|
|
if (key==KEY_HOME) { g_input_cur=0; return; }
|
|
if (key==KEY_END) { g_input_cur=g_input_len; return; }
|
|
|
|
if (key==KEY_UP) {
|
|
if(g_hist_count>0){ if(g_hist_pos<g_hist_count-1)g_hist_pos++; const char *t=g_hist[g_hist_count-1-g_hist_pos].text; strncpy(g_input,t,MAX_INPUT-1); g_input_len=strlen(g_input); g_input_cur=g_input_len; }
|
|
return;
|
|
}
|
|
if (key==KEY_DOWN) {
|
|
if(g_hist_pos>0){ g_hist_pos--; const char *t=g_hist[g_hist_count-1-g_hist_pos].text; strncpy(g_input,t,MAX_INPUT-1); g_input_len=strlen(g_input); g_input_cur=g_input_len; }
|
|
else { g_hist_pos=-1; g_input[0]='\0'; g_input_len=0; g_input_cur=0; }
|
|
return;
|
|
}
|
|
|
|
if (key>=32&&key<256) {
|
|
if(g_input_len<MAX_INPUT-1){ memmove(g_input+g_input_cur+1,g_input+g_input_cur,g_input_len-g_input_cur+1); g_input[g_input_cur++]=(char)key; g_input_len++; }
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* ── main ──────────────────────────────────────────────────────────────────── */
|
|
|
|
static void usage(const char *prog) {
|
|
fprintf(stderr,
|
|
"Usage: %s [options]\n"
|
|
" --host HOST server hostname\n"
|
|
" --port PORT port (default 6697)\n"
|
|
" --nick NICK nickname\n"
|
|
" --channel CHAN channel(s) to join, comma-separated\n"
|
|
" --tls / --no-tls TLS on/off (default on)\n"
|
|
" --sasl-user USER SASL username\n"
|
|
" --sasl-pass PASS SASL password\n"
|
|
" --config FILE config file\n"
|
|
"\n"
|
|
"Multiple servers via ~/.sirc [server] blocks.\n", prog);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *config_path=NULL;
|
|
for(int i=1;i<argc-1;i++) if(strcmp(argv[i],"--config")==0){config_path=argv[i+1];break;}
|
|
load_config(config_path);
|
|
|
|
/* CLI server (only if --host given, or if no servers loaded yet) */
|
|
const char *cli_host=NULL;
|
|
int cli_port=-1; int cli_tls=-1;
|
|
const char *cli_nick=NULL, *cli_chan=NULL, *cli_su=NULL, *cli_sp=NULL;
|
|
for(int i=1;i<argc;i++) {
|
|
if (strcmp(argv[i],"--host")==0 &&i+1<argc) cli_host=argv[++i];
|
|
else if (strcmp(argv[i],"--port")==0 &&i+1<argc) cli_port=atoi(argv[++i]);
|
|
else if (strcmp(argv[i],"--nick")==0 &&i+1<argc) cli_nick=argv[++i];
|
|
else if (strcmp(argv[i],"--channel")==0 &&i+1<argc) cli_chan=argv[++i];
|
|
else if (strcmp(argv[i],"--tls")==0) cli_tls=1;
|
|
else if (strcmp(argv[i],"--no-tls")==0) cli_tls=0;
|
|
else if (strcmp(argv[i],"--sasl-user")==0&&i+1<argc) cli_su=argv[++i];
|
|
else if (strcmp(argv[i],"--sasl-pass")==0&&i+1<argc) cli_sp=argv[++i];
|
|
else if (strcmp(argv[i],"--config")==0) i++;
|
|
else if (strcmp(argv[i],"--help")==0) { usage(argv[0]); return 0; }
|
|
}
|
|
|
|
/* if CLI gave a host, either override first server or add new one */
|
|
if (cli_host) {
|
|
int si;
|
|
if (g_srv_count==0) si=srv_alloc();
|
|
else si=0;
|
|
strncpy(g_srv[si].host,cli_host,MAX_HOST-1);
|
|
if(cli_port>0) g_srv[si].port=cli_port;
|
|
if(cli_tls>=0) g_srv[si].use_tls=cli_tls;
|
|
if(cli_chan) srv_add_autojoin(si,cli_chan);
|
|
if(cli_su) strncpy(g_srv[si].sasl_user,cli_su,MAX_NICK-1);
|
|
if(cli_sp) strncpy(g_srv[si].sasl_pass,cli_sp,255);
|
|
if(cli_nick) strncpy(g_srv[si].nick,cli_nick,MAX_NICK-1);
|
|
} else if (g_srv_count==0) {
|
|
/* no config, no --host: add default server */
|
|
srv_alloc();
|
|
}
|
|
|
|
/* apply --nick / --channel / --tls to all servers that don't have
|
|
their own value set (covers the no-host case) */
|
|
for (int i=0; i<g_srv_count; i++) {
|
|
if (cli_nick && !cli_host) strncpy(g_srv[i].nick, cli_nick, MAX_NICK-1);
|
|
if (cli_tls>=0 && !cli_host) g_srv[i].use_tls = cli_tls;
|
|
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*");
|
|
|
|
/* event pipe */
|
|
if(pipe(g_evpipe)<0){perror("pipe");return 1;}
|
|
fcntl(g_evpipe[1],F_SETFL,O_NONBLOCK);
|
|
fcntl(g_evpipe[0],F_SETFL,O_NONBLOCK);
|
|
signal(SIGPIPE,SIG_IGN);
|
|
|
|
initscr(); raw(); noecho();
|
|
keypad(stdscr,TRUE); nodelay(stdscr,TRUE); curs_set(2);
|
|
init_colors(); build_windows();
|
|
|
|
/* start all server threads */
|
|
for(int i=0;i<g_srv_count;i++) start_net_thread(i);
|
|
|
|
while(1) {
|
|
Event ev;
|
|
while(read(g_evpipe[0],&ev,sizeof(Event))==(ssize_t)sizeof(Event))
|
|
handle_event(&ev);
|
|
draw_all();
|
|
int key=getch();
|
|
if(key!=ERR) handle_key(key);
|
|
struct timespec ts={0,20000000};
|
|
nanosleep(&ts,NULL);
|
|
}
|
|
endwin(); return 0;
|
|
}
|