sirc.c (81363B)
1 /* 2 * sirc.c — multi-server terminal IRC client 3 * 4 * Usage: sirc [options] 5 * 6 * Config (~/.sirc): 7 * # Default server (applied when no [server] block precedes it) 8 * nick = mynick 9 * ignore = badbot,spammer 10 * 11 * [server] 12 * host = irc.libera.chat 13 * port = 6697 14 * nick = mynick 15 * channel = #python,#linux 16 * tls = true 17 * sasl_user = mynick 18 * sasl_pass = hunter2 19 * 20 * [server] 21 * host = irc.oftc.net 22 * port = 6697 23 * tls = true 24 * channel = #debian 25 * 26 * CLI adds one server on top of whatever the config defines: 27 * --host / --port / --nick / --channel / --tls / --no-tls 28 * --sasl-user / --sasl-pass / --config 29 * 30 * Commands: 31 * /join #chan join channel (on current server) 32 * /part [#chan] leave channel 33 * /cycle [#chan] part and rejoin 34 * /nick NAME change nick 35 * /msg NICK TEXT private message 36 * /me TEXT action 37 * /notice TGT TEXT send notice 38 * /ctcp NICK CMD CTCP request 39 * /names [#chan] list users 40 * /topic [text] get/set topic 41 * /whois NICK whois lookup 42 * /who [target] who query 43 * /list [pattern] list channels on server 44 * /away [msg] set away 45 * /back clear away 46 * /invite NICK [chan] invite to channel 47 * /kick NICK [reason] kick from channel 48 * /mode [target] [m] get/set mode 49 * /server HOST [port] connect to new server 50 * /raw <line> send raw IRC line (alias /quote) 51 * /ignore [nick] ignore nick (no arg = list) 52 * /unignore NICK unignore nick 53 * /clear clear scrollback 54 * /connect reconnect current server 55 * /quit [msg] disconnect and exit 56 * /help show help 57 * 58 * Keys: 59 * Tab nick completion (cycle) 60 * Ctrl+N/P next/prev channel (skips server headers) 61 * PgUp/PgDn scroll chat 62 * Ctrl+W delete word left 63 * Up/Down input history 64 */ 65 66 #define _POSIX_C_SOURCE 200809L 67 #define _DEFAULT_SOURCE 68 #define _GNU_SOURCE 69 70 #include <string.h> 71 #include <stdio.h> 72 #include <stdlib.h> 73 #include <stdarg.h> 74 #include <ctype.h> 75 #include <time.h> 76 #include <errno.h> 77 #include <unistd.h> 78 #include <fcntl.h> 79 #include <sys/socket.h> 80 #include <netdb.h> 81 #include <pthread.h> 82 #include <signal.h> 83 #include <netinet/in.h> 84 #include <arpa/inet.h> 85 #include <ncurses.h> 86 #include <openssl/ssl.h> 87 #include <openssl/err.h> 88 89 /* ── constants ─────────────────────────────────────────────────────────────── */ 90 91 #define MAX_NICK 64 92 #define MAX_CHAN 64 93 #define MAX_HOST 256 94 #define MAX_LINE 512 95 #define MAX_INPUT 480 96 #define MAX_HISTORY 500 97 #define MAX_SERVERS 8 98 #define MAX_CHANS_TOTAL 128 /* across all servers */ 99 #define MAX_USERS 512 100 #define MAX_IGNORE 64 101 #define MAX_HIST_LINES 256 102 #define MAX_AUTOJOIN 16 103 #define PING_INTERVAL 90 104 #define CHAN_W 18 105 #define USER_W 16 106 #define RECONNECT_DELAY 5 107 108 /* colour pair ids */ 109 #define C_BORDER 1 110 #define C_HEADER 2 111 #define C_ME 4 112 #define C_JOIN 5 113 #define C_MENTION 6 114 #define C_STATUS 7 115 #define C_INPUT 8 116 #define C_CHAN_SEL 9 117 #define C_CHAN 10 118 #define C_UNREAD 11 119 #define C_MENTION_CHAN 12 120 #define C_URL 13 121 #define C_ACTION 14 122 #define C_SERVER_HDR 15 /* server name row in channel list */ 123 #define C_NICK_BASE 16 124 #define C_NICK_COUNT 8 125 126 /* line flags */ 127 #define F_MSG 0 128 #define F_ME 1 129 #define F_MENTION 2 130 #define F_JOIN 3 131 #define F_STATUS 4 132 #define F_ACTION 5 133 134 /* ── data structures ───────────────────────────────────────────────────────── */ 135 136 typedef struct { 137 char ts[8]; 138 int flag; 139 char text[MAX_LINE]; 140 } Line; 141 142 typedef struct { 143 char name[MAX_CHAN]; 144 int srv; /* index of owning server */ 145 Line lines[MAX_HISTORY]; 146 int line_head; 147 int line_count; 148 char users[MAX_USERS][MAX_NICK]; 149 int user_count; 150 int unread; 151 int mention; 152 int scroll; 153 char topic[MAX_LINE]; 154 int names_pending; /* 1 while receiving fresh 353 NAMES reply */ 155 } Channel; 156 157 typedef struct { 158 char text[MAX_INPUT]; 159 } HistEntry; 160 161 /* IRC event types — each carries a server index so the UI routes correctly */ 162 typedef enum { 163 EV_CONNECTED, EV_STATUS, EV_ERROR, EV_SERVER_TEXT, 164 EV_PRIVMSG, EV_JOIN, EV_PART, EV_QUIT_MSG, 165 EV_NICK_CHANGE, EV_NAMES, EV_KICK, EV_RAW, 166 EV_RECONNECT, EV_TOPIC, EV_NAMES_END 167 } EvType; 168 169 typedef struct { 170 EvType type; 171 int srv; /* server index */ 172 char nick[MAX_NICK]; 173 char chan[MAX_CHAN]; 174 char text[MAX_LINE]; 175 char extra[MAX_NICK]; 176 } Event; 177 178 /* per-server state */ 179 typedef struct { 180 /* config */ 181 char host[MAX_HOST]; 182 int port; 183 char nick[MAX_NICK]; 184 char autojoin[MAX_AUTOJOIN][MAX_CHAN]; 185 int autojoin_count; 186 int use_tls; 187 char sasl_user[MAX_NICK]; 188 char sasl_pass[256]; 189 190 /* runtime */ 191 int connected; 192 volatile int net_stop; 193 pthread_t net_tid; 194 pthread_mutex_t send_lock; 195 int sock; 196 SSL *ssl; 197 198 int reconnect_pending; 199 pthread_t reconnect_tid; 200 int sasl_auth_sent; /* guard: only send AUTHENTICATE once per session */ 201 202 /* per-server event pipe */ 203 int evpipe[2]; 204 } Server; 205 206 /* ── globals ───────────────────────────────────────────────────────────────── */ 207 208 static Server g_srv[MAX_SERVERS]; 209 static int g_srv_count = 0; 210 static SSL_CTX *g_ssl_ctx = NULL; 211 212 /* channels (flat array, each has a .srv index) */ 213 static Channel g_chans[MAX_CHANS_TOTAL]; 214 static int g_chan_count = 0; 215 static int g_active = 0; /* index into g_chans; -1 = no channels */ 216 217 /* global ignore list */ 218 static char g_ignore[MAX_IGNORE][MAX_NICK]; 219 static int g_ignore_count = 0; 220 221 /* input */ 222 static char g_input[MAX_INPUT]; 223 static int g_input_len = 0; 224 static int g_input_cur = 0; 225 static HistEntry g_hist[MAX_HIST_LINES]; 226 static int g_hist_count = 0; 227 static int g_hist_pos = -1; 228 229 /* tab completion */ 230 static char g_tab_matches[MAX_USERS][MAX_NICK]; 231 static int g_tab_count = 0; 232 static int g_tab_idx = 0; 233 static int g_tab_word_start = 0; 234 static int g_tab_active = 0; 235 236 /* single event pipe the UI polls — all servers write here */ 237 static int g_evpipe[2]; 238 239 /* curses windows */ 240 static WINDOW *win_chan = NULL; 241 static WINDOW *win_chat = NULL; 242 static WINDOW *win_users = NULL; 243 static WINDOW *win_input = NULL; 244 static WINDOW *win_status = NULL; 245 246 /* ── utility ───────────────────────────────────────────────────────────────── */ 247 248 static void str_upper(char *dst, const char *src, int n) { 249 int i; 250 for (i = 0; i < n-1 && src[i]; i++) 251 dst[i] = toupper((unsigned char)src[i]); 252 dst[i] = '\0'; 253 } 254 255 static int str_icase_starts(const char *hay, const char *needle) { 256 while (*needle) { 257 if (tolower((unsigned char)*hay) != tolower((unsigned char)*needle)) 258 return 0; 259 hay++; needle++; 260 } 261 return 1; 262 } 263 264 static int nick_colour(const char *nick) { 265 unsigned int h = 0; 266 for (; *nick; nick++) h = (h * 31 + (unsigned char)*nick) & 0xFFFF; 267 return C_NICK_BASE + (h % C_NICK_COUNT); 268 } 269 270 static void timestamp(char *buf, int n) { 271 time_t t = time(NULL); 272 strftime(buf, n, "%H:%M", localtime(&t)); 273 } 274 275 /* ── channel management ────────────────────────────────────────────────────── */ 276 277 static int chan_find(int srv, const char *name) { 278 for (int i = 0; i < g_chan_count; i++) 279 if (g_chans[i].srv == srv && strcasecmp(g_chans[i].name, name) == 0) 280 return i; 281 return -1; 282 } 283 284 static int chan_add(int srv, const char *name) { 285 int i = chan_find(srv, name); 286 if (i >= 0) return i; 287 if (g_chan_count >= MAX_CHANS_TOTAL) return 0; 288 i = g_chan_count++; 289 memset(&g_chans[i], 0, sizeof(Channel)); 290 strncpy(g_chans[i].name, name, MAX_CHAN-1); 291 g_chans[i].srv = srv; 292 return i; 293 } 294 295 static void chan_remove(int idx) { 296 if (idx < 0 || idx >= g_chan_count) return; 297 for (int i = idx; i < g_chan_count-1; i++) 298 g_chans[i] = g_chans[i+1]; 299 g_chan_count--; 300 if (g_active >= g_chan_count) g_active = g_chan_count - 1; 301 if (g_active < 0) g_active = 0; 302 } 303 304 static Line *chan_line(Channel *ch, int idx) { 305 if (idx < 0 || idx >= ch->line_count) return NULL; 306 int real = (ch->line_head - ch->line_count + idx + MAX_HISTORY) % MAX_HISTORY; 307 return &ch->lines[real]; 308 } 309 310 /* strip IRC formatting: \x02 bold, \x03 colour, \x0f reset, 311 \x1d italic, \x1f underline, \x1e strikethrough, \x11 monospace */ 312 static void strip_irc_fmt(const char *in, char *out, int outlen) { 313 int j = 0; 314 for (int i = 0; in[i] && j < outlen-1; i++) { 315 unsigned char c = (unsigned char)in[i]; 316 if (c==0x02||c==0x0f||c==0x1d||c==0x1f||c==0x1e||c==0x11) 317 continue; 318 if (c == 0x03) { /* colour: \x03[fg[,bg]] — skip digits */ 319 i++; 320 if (in[i] && isdigit((unsigned char)in[i])) i++; 321 if (in[i] && isdigit((unsigned char)in[i])) i++; 322 if (in[i] == ',') { 323 i++; 324 if (in[i] && isdigit((unsigned char)in[i])) i++; 325 if (in[i] && isdigit((unsigned char)in[i])) i++; 326 } 327 i--; 328 continue; 329 } 330 out[j++] = (char)c; 331 } 332 out[j] = '\0'; 333 } 334 335 static void chan_addline(Channel *ch, int flag, const char *text) { 336 char ts[8]; timestamp(ts, sizeof(ts)); 337 Line *l = &ch->lines[ch->line_head % MAX_HISTORY]; 338 strncpy(l->ts, ts, sizeof(l->ts)-1); 339 strip_irc_fmt(text, l->text, MAX_LINE); 340 l->flag = flag; 341 ch->line_head = (ch->line_head + 1) % MAX_HISTORY; 342 if (ch->line_count < MAX_HISTORY) ch->line_count++; 343 if (flag != F_STATUS) ch->unread++; 344 } 345 346 /* get the status channel for a server (create if needed) */ 347 static int srv_status_chan(int srv) { 348 return chan_add(srv, "*status*"); 349 } 350 351 static void srv_status_msg(int srv, const char *msg) { 352 int ci = srv_status_chan(srv); 353 chan_addline(&g_chans[ci], F_STATUS, msg); 354 } 355 356 static void srv_status_fmt(int srv, const char *fmt, ...) { 357 char buf[MAX_LINE]; 358 va_list ap; va_start(ap, fmt); 359 vsnprintf(buf, sizeof(buf), fmt, ap); 360 va_end(ap); 361 srv_status_msg(srv, buf); 362 } 363 364 /* Store user as "<prefix><nick>" where prefix is @, +, ~, &, % or space. 365 Lookup is always by bare nick (skip leading mode char). */ 366 static int user_find(Channel *ch, const char *nick) { 367 for (int i = 0; i < ch->user_count; i++) { 368 const char *stored = ch->users[i]; 369 /* skip stored mode prefix */ 370 if (*stored == '@' || *stored == '+' || *stored == '~' || 371 *stored == '&' || *stored == '%' || *stored == ' ') 372 stored++; 373 if (strcasecmp(stored, nick) == 0) return i; 374 } 375 return -1; 376 } 377 378 static void chan_adduser(Channel *ch, char mode, const char *nick) { 379 int idx = user_find(ch, nick); 380 char entry[MAX_NICK]; 381 entry[0] = mode ? mode : ' '; 382 strncpy(entry+1, nick, MAX_NICK-2); 383 if (idx >= 0) { 384 /* update mode in place */ 385 strncpy(ch->users[idx], entry, MAX_NICK-1); 386 return; 387 } 388 if (ch->user_count >= MAX_USERS) return; 389 strncpy(ch->users[ch->user_count++], entry, MAX_NICK-1); 390 } 391 392 static void chan_removeuser(Channel *ch, const char *nick) { 393 int idx = user_find(ch, nick); 394 if (idx < 0) return; 395 for (int j = idx; j < ch->user_count-1; j++) 396 memcpy(ch->users[j], ch->users[j+1], MAX_NICK); 397 ch->user_count--; 398 } 399 400 static int mode_rank(char c) { 401 /* lower = higher privilege */ 402 switch (c) { 403 case '~': return 0; /* founder/owner */ 404 case '&': return 1; /* protected */ 405 case '@': return 2; /* op */ 406 case '%': return 3; /* halfop */ 407 case '+': return 4; /* voice */ 408 default: return 5; /* regular */ 409 } 410 } 411 412 static int user_cmp(const void *a, const void *b) { 413 const char *sa = (const char *)a; 414 const char *sb = (const char *)b; 415 int ra = mode_rank(*sa); 416 int rb = mode_rank(*sb); 417 if (ra != rb) return ra - rb; 418 /* same rank: sort by nick (skip mode prefix) */ 419 const char *na = (*sa == ' ' || mode_rank(*sa) < 5) ? sa+1 : sa; 420 const char *nb = (*sb == ' ' || mode_rank(*sb) < 5) ? sb+1 : sb; 421 return strcasecmp(na, nb); 422 } 423 424 static void chan_sort_users(Channel *ch) { 425 qsort(ch->users, ch->user_count, MAX_NICK, user_cmp); 426 } 427 428 /* next/prev channel index that is NOT a *status* channel (unless it's the 429 only kind), skipping nothing — just a linear wrap */ 430 /* Build the visual order of channels (same traversal as draw_channels). 431 Returns the count; fills idx[] with g_chans indices in display order. */ 432 static int visual_order(int *idx, int max) { 433 int n = 0; 434 for (int si = 0; si < g_srv_count && n < max; si++) { 435 /* *status* first */ 436 for (int ci = 0; ci < g_chan_count && n < max; ci++) { 437 if (g_chans[ci].srv != si) continue; 438 if (strcmp(g_chans[ci].name, "*status*") == 0) 439 idx[n++] = ci; 440 } 441 /* then regular channels */ 442 for (int ci = 0; ci < g_chan_count && n < max; ci++) { 443 if (g_chans[ci].srv != si) continue; 444 if (strcmp(g_chans[ci].name, "*status*") != 0) 445 idx[n++] = ci; 446 } 447 } 448 return n; 449 } 450 451 static int chan_next(int cur, int dir) { 452 if (g_chan_count <= 1) return cur; 453 int idx[MAX_CHANS_TOTAL]; 454 int n = visual_order(idx, MAX_CHANS_TOTAL); 455 if (n == 0) return cur; 456 /* find cur in visual order */ 457 int pos = 0; 458 for (int i = 0; i < n; i++) 459 if (idx[i] == cur) { pos = i; break; } 460 pos = (pos + dir + n) % n; 461 return idx[pos]; 462 } 463 464 /* ── ignore list ───────────────────────────────────────────────────────────── */ 465 466 static int is_ignored(const char *nick) { 467 for (int i = 0; i < g_ignore_count; i++) 468 if (strcasecmp(g_ignore[i], nick) == 0) return 1; 469 return 0; 470 } 471 472 static void ignore_add(const char *nick) { 473 if (is_ignored(nick) || g_ignore_count >= MAX_IGNORE) return; 474 strncpy(g_ignore[g_ignore_count++], nick, MAX_NICK-1); 475 } 476 477 static void ignore_remove(const char *nick) { 478 for (int i = 0; i < g_ignore_count; i++) { 479 if (strcasecmp(g_ignore[i], nick) == 0) { 480 for (int j = i; j < g_ignore_count-1; j++) 481 memcpy(g_ignore[j], g_ignore[j+1], MAX_NICK); 482 g_ignore_count--; 483 return; 484 } 485 } 486 } 487 488 /* ── event pipe ────────────────────────────────────────────────────────────── */ 489 490 static void ev_push(const Event *ev) { 491 write(g_evpipe[1], ev, sizeof(Event)); 492 } 493 494 static void ev_simple(EvType type, int srv, const char *nick, 495 const char *chan, const char *text) { 496 Event ev; memset(&ev, 0, sizeof(ev)); 497 ev.type = type; ev.srv = srv; 498 if (nick) strncpy(ev.nick, nick, MAX_NICK-1); 499 if (chan) strncpy(ev.chan, chan, MAX_CHAN-1); 500 if (text) strncpy(ev.text, text, MAX_LINE-1); 501 ev_push(&ev); 502 } 503 504 /* ── network send ──────────────────────────────────────────────────────────── */ 505 506 static int srv_send_raw(int si, const char *line) { 507 char buf[MAX_LINE]; 508 int n = snprintf(buf, sizeof(buf), "%s\r\n", line); 509 if (n <= 0 || n >= (int)sizeof(buf)) return -1; 510 Server *s = &g_srv[si]; 511 pthread_mutex_lock(&s->send_lock); 512 int r; 513 if (s->use_tls && s->ssl) 514 r = SSL_write(s->ssl, buf, n); 515 else 516 r = send(s->sock, buf, n, 0); 517 pthread_mutex_unlock(&s->send_lock); 518 return r > 0 ? 0 : -1; 519 } 520 521 static void srv_sendf(int si, const char *fmt, ...) { 522 char buf[MAX_LINE]; 523 va_list ap; va_start(ap, fmt); 524 vsnprintf(buf, sizeof(buf), fmt, ap); 525 va_end(ap); 526 srv_send_raw(si, buf); 527 } 528 529 /* ── IRC parser ────────────────────────────────────────────────────────────── */ 530 531 typedef struct { 532 char prefix[MAX_LINE]; 533 char cmd[32]; 534 char params[16][MAX_LINE]; 535 int nparams; 536 char trail[MAX_LINE]; 537 } IrcMsg; 538 539 static void nick_from_prefix(const char *prefix, char *nick, int n) { 540 const char *bang = strchr(prefix, '!'); 541 if (bang) { 542 int len = (int)(bang - prefix); 543 if (len >= n) len = n-1; 544 memcpy(nick, prefix, len); nick[len] = '\0'; 545 } else { 546 strncpy(nick, prefix, n-1); nick[n-1] = '\0'; 547 } 548 } 549 550 static void parse_irc(const char *raw, IrcMsg *m) { 551 memset(m, 0, sizeof(*m)); 552 const char *p = raw; 553 if (*p == ':') { 554 p++; 555 const char *end = strchr(p, ' '); if (!end) return; 556 int len = (int)(end-p); if (len>=(int)sizeof(m->prefix)) len=sizeof(m->prefix)-1; 557 memcpy(m->prefix, p, len); p = end+1; 558 } 559 { 560 const char *end = strchr(p, ' '); 561 int len = end ? (int)(end-p) : (int)strlen(p); 562 if (len>=(int)sizeof(m->cmd)) len=sizeof(m->cmd)-1; 563 memcpy(m->cmd, p, len); str_upper(m->cmd, m->cmd, sizeof(m->cmd)); 564 if (!end) return; p = end+1; 565 } 566 while (*p) { 567 if (*p == ':') { strncpy(m->trail, p+1, MAX_LINE-1); break; } 568 const char *end = strchr(p, ' '); 569 int len = end ? (int)(end-p) : (int)strlen(p); 570 if (m->nparams < 16) { 571 if (len>=MAX_LINE) len=MAX_LINE-1; 572 memcpy(m->params[m->nparams], p, len); m->nparams++; 573 } 574 if (!end) break; p = end+1; 575 } 576 } 577 578 /* ── base64 for SASL ───────────────────────────────────────────────────────── */ 579 580 static const char b64tab[] = 581 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 582 583 static void base64_encode(const unsigned char *in, int inlen, char *out) { 584 int j = 0; 585 for (int i = 0; i < inlen; i += 3) { 586 unsigned int a = (unsigned char)in[i]; 587 unsigned int b = (i+1 < inlen) ? (unsigned char)in[i+1] : 0; 588 unsigned int c = (i+2 < inlen) ? (unsigned char)in[i+2] : 0; 589 unsigned int n = (a << 16) | (b << 8) | c; 590 out[j++] = b64tab[(n >> 18) & 63]; 591 out[j++] = b64tab[(n >> 12) & 63]; 592 out[j++] = (i+1 < inlen) ? b64tab[(n >> 6) & 63] : '='; 593 out[j++] = (i+2 < inlen) ? b64tab[n & 63] : '='; 594 } 595 out[j] = '\0'; 596 } 597 598 /* ── IRC line handler (net thread — uses si to tag every event) ────────────── */ 599 600 static void handle_irc_line(int si, const char *raw) { 601 IrcMsg m; parse_irc(raw, &m); 602 char nick[MAX_NICK]=""; nick_from_prefix(m.prefix, nick, sizeof(nick)); 603 Server *s = &g_srv[si]; 604 605 if (strcmp(m.cmd,"PING")==0) { 606 srv_sendf(si, "PONG :%s", m.trail[0]?m.trail:m.params[0]); 607 return; 608 } 609 610 if (strcmp(m.cmd,"PONG")==0) return; /* ignore server's PONG replies */ 611 612 /* SASL / CAP negotiation */ 613 if (strcmp(m.cmd,"CAP")==0) { 614 char sub[16]=""; 615 if (m.nparams>=2) str_upper(sub, m.params[1], sizeof(sub)); 616 else if (m.nparams>=1) str_upper(sub, m.params[0], sizeof(sub)); 617 618 /* log all CAP lines to status for visibility */ 619 char capdbg[MAX_LINE]; 620 snprintf(capdbg,sizeof(capdbg),"[CAP] %s",raw); 621 ev_simple(EV_STATUS, si, NULL, NULL, capdbg); 622 623 if (strcmp(sub,"LS")==0) { 624 /* server advertising capabilities -- request sasl */ 625 srv_send_raw(si, "CAP REQ :sasl"); 626 } else if (strcmp(sub,"ACK")==0) { 627 const char *caps = m.trail[0] ? m.trail : (m.nparams>0?m.params[m.nparams-1]:""); 628 if (strstr(caps,"sasl")) { 629 if (!s->sasl_auth_sent) { 630 s->sasl_auth_sent = 1; 631 srv_send_raw(si, "AUTHENTICATE PLAIN"); 632 } 633 } else { 634 srv_send_raw(si, "CAP END"); 635 } 636 } else if (strcmp(sub,"NAK")==0) { 637 ev_simple(EV_ERROR, si, NULL, NULL, "Server rejected SASL CAP -- connecting without SASL."); 638 srv_send_raw(si, "CAP END"); 639 srv_sendf(si, "NICK %s", s->nick); 640 srv_sendf(si, "USER %s 0 * :sirc", s->nick); 641 } 642 return; 643 } 644 if (strcmp(m.cmd,"AUTHENTICATE")==0) { 645 /* server sends AUTHENTICATE + (trail or param) to prompt us */ 646 const char *challenge = m.trail[0] ? m.trail : 647 (m.nparams>0 ? m.params[0] : ""); 648 if (strcmp(challenge,"+")==0) { 649 /* build PLAIN payload: authzid\0authcid\0passwd 650 must use memcpy — sasl_user/pass may not contain \0 651 but we need to embed \0 separators manually */ 652 char payload[512]; 653 int ulen = strlen(s->sasl_user); 654 int plen_pass = strlen(s->sasl_pass); 655 /* layout: [sasl_user]\0[sasl_user]\0[sasl_pass] */ 656 int total = ulen + 1 + ulen + 1 + plen_pass; 657 if (total < (int)sizeof(payload)) { 658 int pos = 0; 659 memcpy(payload + pos, s->sasl_user, ulen); pos += ulen; 660 payload[pos++] = '\0'; 661 memcpy(payload + pos, s->sasl_user, ulen); pos += ulen; 662 payload[pos++] = '\0'; 663 memcpy(payload + pos, s->sasl_pass, plen_pass); pos += plen_pass; 664 char enc[700]; 665 base64_encode((unsigned char*)payload, total, enc); 666 srv_sendf(si, "AUTHENTICATE %s", enc); 667 } 668 } 669 return; 670 } 671 if (strcmp(m.cmd,"903")==0) { 672 ev_simple(EV_STATUS, si, NULL, NULL, "SASL authentication successful."); 673 srv_send_raw(si, "CAP END"); 674 srv_sendf(si, "NICK %s", s->nick); 675 srv_sendf(si, "USER %s 0 * :sirc", s->nick); 676 return; 677 } 678 if (strcmp(m.cmd,"902")==0||strcmp(m.cmd,"904")==0|| 679 strcmp(m.cmd,"905")==0||strcmp(m.cmd,"906")==0) { 680 char buf[MAX_LINE]; 681 snprintf(buf,sizeof(buf),"SASL failed (%s): %s",m.cmd,m.trail); 682 ev_simple(EV_ERROR, si, NULL, NULL, buf); 683 srv_send_raw(si, "CAP END"); 684 srv_sendf(si, "NICK %s", s->nick); 685 srv_sendf(si, "USER %s 0 * :sirc", s->nick); 686 return; 687 } 688 689 /* 001 */ 690 if (strcmp(m.cmd,"001")==0) { 691 s->connected=1; 692 ev_simple(EV_CONNECTED, si, NULL, NULL, m.trail); 693 ev_simple(EV_SERVER_TEXT, si, NULL, NULL, m.trail); 694 return; 695 } 696 697 /* PRIVMSG */ 698 if (strcmp(m.cmd,"PRIVMSG")==0) { 699 Event ev; memset(&ev,0,sizeof(ev)); 700 ev.type=EV_PRIVMSG; ev.srv=si; 701 strncpy(ev.nick, nick, MAX_NICK-1); 702 strncpy(ev.chan, m.nparams>0?m.params[0]:"", MAX_CHAN-1); 703 strncpy(ev.text, m.trail, MAX_LINE-1); 704 ev_push(&ev); return; 705 } 706 707 /* JOIN */ 708 if (strcmp(m.cmd,"JOIN")==0) { 709 const char *ch=m.trail[0]?m.trail:(m.nparams>0?m.params[0]:""); 710 ev_simple(EV_JOIN, si, nick, ch, NULL); return; 711 } 712 713 /* PART */ 714 if (strcmp(m.cmd,"PART")==0) { 715 ev_simple(EV_PART, si, nick, m.nparams>0?m.params[0]:"", m.trail); 716 return; 717 } 718 719 /* QUIT */ 720 if (strcmp(m.cmd,"QUIT")==0) { 721 ev_simple(EV_QUIT_MSG, si, nick, NULL, m.trail); return; 722 } 723 724 /* NICK */ 725 if (strcmp(m.cmd,"NICK")==0) { 726 Event ev; memset(&ev,0,sizeof(ev)); 727 ev.type=EV_NICK_CHANGE; ev.srv=si; 728 strncpy(ev.nick, nick, MAX_NICK-1); 729 const char *nn=m.trail[0]?m.trail:(m.nparams>0?m.params[0]:""); 730 strncpy(ev.extra, nn, MAX_NICK-1); 731 ev_push(&ev); return; 732 } 733 734 /* 353 NAMES */ 735 if (strcmp(m.cmd,"353")==0) { 736 const char *ch=m.nparams>=3?m.params[2]:""; 737 char tmp[MAX_LINE]; strncpy(tmp,m.trail,MAX_LINE-1); 738 char *tok=strtok(tmp," "); 739 while (tok) { 740 Event ev; memset(&ev,0,sizeof(ev)); 741 ev.type=EV_NAMES; ev.srv=si; 742 strncpy(ev.chan, ch, MAX_CHAN-1); 743 /* ev.extra[0] = mode prefix char (or 0 if none) */ 744 const char *n=tok; 745 if (*n=='@'||*n=='+'||*n=='~'||*n=='&'||*n=='%') { 746 ev.extra[0]=*n; n++; 747 } 748 strncpy(ev.nick, n, MAX_NICK-1); 749 ev_push(&ev); 750 tok=strtok(NULL," "); 751 } 752 return; 753 } 754 755 /* KICK */ 756 if (strcmp(m.cmd,"KICK")==0) { 757 Event ev; memset(&ev,0,sizeof(ev)); 758 ev.type=EV_KICK; ev.srv=si; 759 strncpy(ev.nick, nick, MAX_NICK-1); 760 strncpy(ev.chan, m.nparams>0?m.params[0]:"", MAX_CHAN-1); 761 strncpy(ev.extra, m.nparams>1?m.params[1]:"", MAX_NICK-1); 762 strncpy(ev.text, m.trail, MAX_LINE-1); 763 ev_push(&ev); return; 764 } 765 766 /* 433 nick in use */ 767 if (strcmp(m.cmd,"433")==0) { 768 ev_simple(EV_ERROR, si, NULL, NULL, "Nickname already in use."); 769 return; 770 } 771 772 /* server text */ 773 if (strcmp(m.cmd,"002")==0||strcmp(m.cmd,"003")==0||strcmp(m.cmd,"004")==0|| 774 strcmp(m.cmd,"375")==0||strcmp(m.cmd,"372")==0||strcmp(m.cmd,"376")==0) { 775 ev_simple(EV_SERVER_TEXT, si, NULL, NULL, 776 m.trail[0]?m.trail:m.params[m.nparams>1?1:0]); 777 return; 778 } 779 780 /* MODE */ 781 if (strcmp(m.cmd,"MODE")==0) { 782 char buf[MAX_LINE]; 783 snprintf(buf,sizeof(buf),"MODE %s %s",m.params[0],m.trail); 784 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 785 } 786 787 /* TOPIC */ 788 if (strcmp(m.cmd,"331")==0) { 789 const char *ch=m.nparams>1?m.params[1]:m.params[0]; 790 char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] No topic set",ch); 791 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 792 } 793 if (strcmp(m.cmd,"332")==0) { 794 const char *ch=m.nparams>1?m.params[1]:m.params[0]; 795 char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] Topic: %s",ch,m.trail); 796 ev_simple(EV_STATUS, si, NULL, NULL, buf); 797 ev_simple(EV_TOPIC, si, NULL, ch, m.trail); 798 return; 799 } 800 if (strcmp(m.cmd,"333")==0) { 801 const char *ch=m.nparams>1?m.params[1]:""; 802 const char *setter=m.nparams>2?m.params[2]:""; 803 char sn[MAX_NICK]; strncpy(sn,setter,MAX_NICK-1); 804 char *bang=strchr(sn,'!'); if(bang)*bang='\0'; 805 char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[%s] Topic set by %s",ch,sn); 806 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 807 } 808 if (strcmp(m.cmd,"TOPIC")==0) { 809 const char *ch=m.nparams>0?m.params[0]:""; 810 char buf[MAX_LINE]; 811 snprintf(buf,sizeof(buf),"[%s] %s changed topic to: %s",ch,nick,m.trail); 812 ev_simple(EV_STATUS, si, NULL, NULL, buf); 813 ev_simple(EV_SERVER_TEXT, si, NULL, ch, buf); 814 ev_simple(EV_TOPIC, si, NULL, ch, m.trail); 815 return; 816 } 817 818 /* WHOIS */ 819 if (strcmp(m.cmd,"311")==0) { 820 char buf[MAX_LINE]; 821 snprintf(buf,sizeof(buf),"[whois] %s (%s@%s): %s", 822 m.nparams>1?m.params[1]:"",m.nparams>2?m.params[2]:"", 823 m.nparams>3?m.params[3]:"",m.trail); 824 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 825 } 826 if (strcmp(m.cmd,"312")==0) { 827 char buf[MAX_LINE]; 828 snprintf(buf,sizeof(buf),"[whois] server: %s (%s)", 829 m.nparams>2?m.params[2]:"",m.trail); 830 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 831 } 832 if (strcmp(m.cmd,"313")==0) { 833 char buf[MAX_LINE]; 834 snprintf(buf,sizeof(buf),"[whois] %s is an IRC operator", 835 m.nparams>1?m.params[1]:""); 836 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 837 } 838 if (strcmp(m.cmd,"317")==0) { 839 int idle=m.nparams>2?atoi(m.params[2]):0; 840 char buf[MAX_LINE]; 841 snprintf(buf,sizeof(buf),"[whois] idle: %dm%ds",idle/60,idle%60); 842 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 843 } 844 if (strcmp(m.cmd,"318")==0) { 845 ev_simple(EV_STATUS, si, NULL, NULL, "[whois] end"); return; 846 } 847 if (strcmp(m.cmd,"319")==0) { 848 char buf[MAX_LINE]; 849 snprintf(buf,sizeof(buf),"[whois] channels: %s",m.trail); 850 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 851 } 852 if (strcmp(m.cmd,"307")==0||strcmp(m.cmd,"330")==0) { 853 char buf[MAX_LINE]; snprintf(buf,sizeof(buf),"[whois] %s",m.trail); 854 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 855 } 856 857 /* WHO */ 858 if (strcmp(m.cmd,"352")==0) { 859 char buf[MAX_LINE]; 860 snprintf(buf,sizeof(buf),"[who] %-16s %s!%s@%s", 861 m.nparams>1?m.params[1]:"*", 862 m.nparams>6?m.params[6]:"", 863 m.nparams>2?m.params[2]:"", 864 m.nparams>3?m.params[3]:""); 865 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 866 } 867 if (strcmp(m.cmd,"315")==0) { 868 ev_simple(EV_STATUS, si, NULL, NULL, "[who] end"); return; 869 } 870 871 /* LIST */ 872 if (strcmp(m.cmd,"321")==0) { 873 ev_simple(EV_STATUS, si, NULL, NULL, "[list] Channel Users Topic"); 874 return; 875 } 876 if (strcmp(m.cmd,"322")==0) { 877 char buf[MAX_LINE]; 878 snprintf(buf,sizeof(buf),"[list] %-20s %-6s %s", 879 m.nparams>1?m.params[1]:"", 880 m.nparams>2?m.params[2]:"",m.trail); 881 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 882 } 883 if (strcmp(m.cmd,"323")==0) { 884 ev_simple(EV_STATUS, si, NULL, NULL, "[list] end"); return; 885 } 886 887 if (strcmp(m.cmd,"366")==0) { 888 /* end of NAMES — reset pending flag so next /names clears again */ 889 const char *ch = m.nparams>=2 ? m.params[1] : (m.nparams>=1 ? m.params[0] : ""); 890 ev_simple(EV_NAMES_END, si, NULL, ch, NULL); 891 return; 892 } 893 894 /* AWAY */ 895 if (strcmp(m.cmd,"301")==0) { 896 char buf[MAX_LINE]; 897 snprintf(buf,sizeof(buf),"[away] %s is away: %s", 898 m.nparams>1?m.params[1]:"",m.trail); 899 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 900 } 901 if (strcmp(m.cmd,"305")==0) { 902 ev_simple(EV_STATUS, si, NULL, NULL, "You are no longer marked as away."); 903 return; 904 } 905 if (strcmp(m.cmd,"306")==0) { 906 ev_simple(EV_STATUS, si, NULL, NULL, "You have been marked as away."); 907 return; 908 } 909 910 /* INVITE */ 911 if (strcmp(m.cmd,"INVITE")==0) { 912 char buf[MAX_LINE]; 913 snprintf(buf,sizeof(buf),"** %s invites you to %s",nick, 914 m.trail[0]?m.trail:m.params[m.nparams>1?1:0]); 915 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 916 } 917 if (strcmp(m.cmd,"341")==0) { 918 char buf[MAX_LINE]; 919 snprintf(buf,sizeof(buf),"Invited %s to %s", 920 m.nparams>1?m.params[1]:"",m.nparams>2?m.params[2]:m.trail); 921 ev_simple(EV_STATUS, si, NULL, NULL, buf); return; 922 } 923 924 /* NOTICE */ 925 if (strcmp(m.cmd,"NOTICE")==0) { 926 const char *target=m.nparams>0?m.params[0]:""; 927 char buf[MAX_LINE]; 928 snprintf(buf,sizeof(buf),"--%s-- %s",nick[0]?nick:"server",m.trail); 929 if (target[0]=='#') { 930 Event ev2; memset(&ev2,0,sizeof(ev2)); 931 ev2.type=EV_SERVER_TEXT; ev2.srv=si; 932 strncpy(ev2.chan, target, MAX_CHAN-1); 933 strncpy(ev2.text, buf, MAX_LINE-1); 934 ev_push(&ev2); 935 } else { 936 ev_simple(EV_STATUS, si, NULL, NULL, buf); 937 } 938 return; 939 } 940 941 ev_simple(EV_RAW, si, NULL, NULL, raw); 942 } 943 944 /* ── network thread ────────────────────────────────────────────────────────── */ 945 946 static void srv_close(int si) { 947 Server *s=&g_srv[si]; 948 if (s->use_tls && s->ssl) { SSL_shutdown(s->ssl); SSL_free(s->ssl); s->ssl=NULL; } 949 if (s->sock>=0) { close(s->sock); s->sock=-1; } 950 s->connected=0; 951 } 952 953 static int srv_connect(int si) { 954 Server *s=&g_srv[si]; 955 char portstr[8]; snprintf(portstr,sizeof(portstr),"%d",s->port); 956 struct addrinfo hints,*res,*r; 957 memset(&hints,0,sizeof(hints)); 958 hints.ai_family=AF_UNSPEC; hints.ai_socktype=SOCK_STREAM; 959 if (getaddrinfo(s->host,portstr,&hints,&res)!=0) return -1; 960 s->sock=-1; 961 for (r=res; r; r=r->ai_next) { 962 int fd=socket(r->ai_family,r->ai_socktype,r->ai_protocol); 963 if (fd<0) continue; 964 int flags=fcntl(fd,F_GETFL,0); 965 fcntl(fd,F_SETFL,flags|O_NONBLOCK); 966 int rc=connect(fd,r->ai_addr,r->ai_addrlen); 967 if (rc==0||errno==EINPROGRESS) { 968 fd_set wfds; FD_ZERO(&wfds); FD_SET(fd,&wfds); 969 struct timeval tv={15,0}; 970 if (select(fd+1,NULL,&wfds,NULL,&tv)>0) { 971 int err=0; socklen_t el=sizeof(err); 972 getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&el); 973 if (err==0) { s->sock=fd; break; } 974 } 975 } 976 close(fd); 977 } 978 freeaddrinfo(res); 979 if (s->sock<0) return -1; 980 int flags=fcntl(s->sock,F_GETFL,0); 981 fcntl(s->sock,F_SETFL,flags&~O_NONBLOCK); 982 983 if (s->use_tls) { 984 if (!g_ssl_ctx) { 985 SSL_library_init(); SSL_load_error_strings(); 986 g_ssl_ctx=SSL_CTX_new(TLS_client_method()); 987 if (!g_ssl_ctx) return -1; 988 SSL_CTX_set_verify(g_ssl_ctx,SSL_VERIFY_PEER,NULL); 989 SSL_CTX_set_default_verify_paths(g_ssl_ctx); 990 } 991 s->ssl=SSL_new(g_ssl_ctx); 992 SSL_set_fd(s->ssl,s->sock); 993 SSL_set_tlsext_host_name(s->ssl,s->host); 994 if (SSL_connect(s->ssl)<=0) { 995 SSL_free(s->ssl); s->ssl=NULL; return -1; 996 } 997 } 998 return 0; 999 } 1000 1001 typedef struct { int si; } NetArg; 1002 1003 static void *net_thread(void *arg) { 1004 NetArg *na=(NetArg*)arg; int si=na->si; free(na); 1005 Server *s=&g_srv[si]; 1006 1007 char buf[MAX_LINE]; 1008 snprintf(buf,sizeof(buf),"Connecting to %s:%d%s...", 1009 s->host,s->port,s->use_tls?" (TLS)":""); 1010 ev_simple(EV_STATUS, si, NULL, NULL, buf); 1011 1012 if (srv_connect(si)<0) { 1013 ev_simple(EV_ERROR, si, NULL, NULL, "Connection failed."); 1014 ev_simple(EV_RECONNECT, si, NULL, NULL, NULL); 1015 return NULL; 1016 } 1017 1018 int sasl_pending=(s->sasl_user[0]&&s->sasl_pass[0])?1:0; 1019 s->sasl_auth_sent = 0; 1020 if (sasl_pending) { 1021 /* CAP LS 302: proper negotiation, handler sends CAP REQ :sasl on LS reply */ 1022 srv_send_raw(si,"CAP LS 302"); 1023 } else { 1024 srv_sendf(si,"NICK %s",s->nick); 1025 srv_sendf(si,"USER %s 0 * :sirc",s->nick); 1026 } 1027 1028 char readbuf[8192], linebuf[MAX_LINE*4]; 1029 int linelen=0; 1030 time_t last_ping=time(NULL); 1031 1032 while (!s->net_stop) { 1033 if (time(NULL)-last_ping>PING_INTERVAL) { 1034 srv_sendf(si,"PING :%s",s->host); 1035 last_ping=time(NULL); 1036 } 1037 fd_set rfds; FD_ZERO(&rfds); FD_SET(s->sock,&rfds); 1038 struct timeval tv={5,0}; 1039 int sel=select(s->sock+1,&rfds,NULL,NULL,&tv); 1040 if (sel==0) continue; 1041 if (sel<0) { ev_simple(EV_ERROR,si,NULL,NULL,"Select error."); break; } 1042 1043 int n; 1044 if (s->use_tls && s->ssl) 1045 n=SSL_read(s->ssl,readbuf,sizeof(readbuf)-1); 1046 else 1047 n=recv(s->sock,readbuf,sizeof(readbuf)-1,0); 1048 if (n<=0) { ev_simple(EV_ERROR,si,NULL,NULL,"Server closed connection."); break; } 1049 readbuf[n]='\0'; 1050 1051 for (int i=0; i<n; i++) { 1052 char c=readbuf[i]; 1053 if (c=='\n') { 1054 if (linelen>0 && linebuf[linelen-1]=='\r') linelen--; 1055 linebuf[linelen]='\0'; 1056 if (linelen>0) handle_irc_line(si,linebuf); 1057 linelen=0; 1058 } else if (linelen<(int)sizeof(linebuf)-1) { 1059 linebuf[linelen++]=c; 1060 } 1061 } 1062 last_ping=time(NULL); 1063 } 1064 1065 srv_close(si); 1066 ev_simple(EV_RECONNECT, si, NULL, NULL, NULL); 1067 return NULL; 1068 } 1069 1070 /* ── reconnect ─────────────────────────────────────────────────────────────── */ 1071 1072 static void start_net_thread(int si); 1073 1074 typedef struct { int si; } ReconArg; 1075 static void *reconnect_thread(void *arg) { 1076 ReconArg *ra=(ReconArg*)arg; int si=ra->si; free(ra); 1077 sleep(RECONNECT_DELAY); 1078 g_srv[si].reconnect_pending=0; 1079 start_net_thread(si); 1080 return NULL; 1081 } 1082 1083 static void start_net_thread(int si) { 1084 g_srv[si].net_stop=0; 1085 NetArg *na=malloc(sizeof(NetArg)); na->si=si; 1086 pthread_create(&g_srv[si].net_tid, NULL, net_thread, na); 1087 } 1088 1089 static void schedule_reconnect(int si) { 1090 if (g_srv[si].reconnect_pending) return; 1091 g_srv[si].reconnect_pending=1; 1092 srv_status_fmt(si,"Reconnecting in %ds...",RECONNECT_DELAY); 1093 ReconArg *ra=malloc(sizeof(ReconArg)); ra->si=si; 1094 pthread_create(&g_srv[si].reconnect_tid, NULL, reconnect_thread, ra); 1095 pthread_detach(g_srv[si].reconnect_tid); 1096 } 1097 1098 /* ── config file ───────────────────────────────────────────────────────────── */ 1099 1100 static int cfg_bool(const char *v) { 1101 return strcasecmp(v,"true")==0||strcasecmp(v,"yes")==0|| 1102 strcasecmp(v,"1")==0||strcasecmp(v,"on")==0; 1103 } 1104 1105 /* add a server entry with defaults; returns its index */ 1106 static int srv_alloc(void) { 1107 if (g_srv_count>=MAX_SERVERS) return -1; 1108 int si=g_srv_count++; 1109 Server *s=&g_srv[si]; 1110 memset(s,0,sizeof(Server)); 1111 strncpy(s->host,"irc.libera.chat",MAX_HOST-1); 1112 s->port=6697; 1113 strncpy(s->nick,"circ_user",MAX_NICK-1); 1114 s->use_tls=1; 1115 s->sock=-1; 1116 pthread_mutex_init(&s->send_lock,NULL); 1117 return si; 1118 } 1119 1120 static void srv_add_autojoin(int si, const char *chanlist) { 1121 char tmp[MAX_LINE]; strncpy(tmp, chanlist, MAX_LINE-1); 1122 char *tok = strtok(tmp, ","); 1123 while (tok) { 1124 /* trim leading and trailing spaces */ 1125 while (*tok == ' ') tok++; 1126 char *end = tok + strlen(tok) - 1; 1127 while (end > tok && *end == ' ') *end-- = '\0'; 1128 if (*tok && g_srv[si].autojoin_count < MAX_AUTOJOIN) { 1129 char ch[MAX_CHAN]; strncpy(ch, tok, MAX_CHAN-1); 1130 if (ch[0] != '#') { memmove(ch+1, ch, strlen(ch)+1); ch[0]='#'; } 1131 /* avoid duplicates */ 1132 int dup = 0; 1133 for (int i = 0; i < g_srv[si].autojoin_count; i++) 1134 if (strcasecmp(g_srv[si].autojoin[i], ch) == 0) { dup=1; break; } 1135 if (!dup) 1136 strncpy(g_srv[si].autojoin[g_srv[si].autojoin_count++], ch, MAX_CHAN-1); 1137 } 1138 tok = strtok(NULL, ","); 1139 } 1140 } 1141 1142 static void load_config(const char *path) { 1143 char candidates[2][MAX_HOST]; 1144 const char *home=getenv("HOME"); 1145 if (home) { 1146 snprintf(candidates[0],MAX_HOST,"%s/.sirc",home); 1147 snprintf(candidates[1],MAX_HOST,"%s/.config/sirc/config",home); 1148 } 1149 FILE *f=NULL; 1150 if (path) f=fopen(path,"r"); 1151 else for (int i=0;i<2&&!f;i++) f=fopen(candidates[i],"r"); 1152 if (!f) return; 1153 1154 /* defaults that apply before any [server] block */ 1155 char def_nick[MAX_NICK]="circ_user"; 1156 1157 /* current server being parsed; -1 = not inside a [server] block */ 1158 int cur_si=-1; 1159 1160 char line[512]; 1161 while (fgets(line,sizeof(line),f)) { 1162 line[strcspn(line,"\r\n")]='\0'; 1163 char *p=line; 1164 while (*p==' '||*p=='\t') p++; 1165 if (!*p||*p=='#') continue; 1166 1167 /* [server] section header */ 1168 if (strcmp(p,"[server]")==0) { 1169 cur_si=srv_alloc(); 1170 if (cur_si>=0) strncpy(g_srv[cur_si].nick,def_nick,MAX_NICK-1); 1171 continue; 1172 } 1173 1174 char *eq=strchr(p,'='); if (!eq) continue; 1175 *eq='\0'; 1176 char *key=p, *val=eq+1; 1177 char *ke=key+strlen(key)-1; 1178 while (ke>key&&(*ke==' '||*ke=='\t')) *ke--='\0'; 1179 while (*val==' '||*val=='\t') val++; 1180 char *ve=val+strlen(val)-1; 1181 while (ve>val&&(*ve==' '||*ve=='\t')) *ve--='\0'; 1182 1183 if (cur_si<0) { 1184 /* global defaults */ 1185 if (strcmp(key,"nick")==0) strncpy(def_nick,val,MAX_NICK-1); 1186 else if (strcmp(key,"ignore")==0) { 1187 char tmp[512]; strncpy(tmp,val,511); 1188 char *tok=strtok(tmp,","); 1189 while (tok) { while(*tok==' ')tok++; if(*tok)ignore_add(tok); tok=strtok(NULL,","); } 1190 } 1191 } else { 1192 Server *s=&g_srv[cur_si]; 1193 if (strcmp(key,"host")==0) strncpy(s->host,val,MAX_HOST-1); 1194 else if (strcmp(key,"port")==0) s->port=atoi(val); 1195 else if (strcmp(key,"nick")==0) strncpy(s->nick,val,MAX_NICK-1); 1196 else if (strcmp(key,"tls")==0) s->use_tls=cfg_bool(val); 1197 else if (strcmp(key,"sasl_user")==0) strncpy(s->sasl_user,val,MAX_NICK-1); 1198 else if (strcmp(key,"sasl_pass")==0) strncpy(s->sasl_pass,val,255); 1199 else if (strcmp(key,"channel")==0) srv_add_autojoin(cur_si,val); 1200 } 1201 } 1202 fclose(f); 1203 } 1204 1205 /* ── URL detection ─────────────────────────────────────────────────────────── */ 1206 1207 static int find_url(const char *str, int start, int *len) { 1208 for (int i=start; str[i]; i++) { 1209 if (strncmp(str+i,"http://",7)==0||strncmp(str+i,"https://",8)==0|| 1210 strncmp(str+i,"www.",4)==0) { 1211 int j=i; 1212 while (str[j]&&str[j]!=' '&&str[j]!='\t'&& 1213 str[j]!='"'&&str[j]!='\''&&str[j]!='<') j++; 1214 *len=j-i; return i; 1215 } 1216 } 1217 return -1; 1218 } 1219 1220 /* ── draw routines ─────────────────────────────────────────────────────────── */ 1221 1222 static void draw_status_bar(void) { 1223 int H,W; getmaxyx(stdscr,H,W); (void)H; 1224 werase(win_status); 1225 wbkgd(win_status,COLOR_PAIR(C_HEADER)); 1226 Channel *ch=&g_chans[g_active]; 1227 /* topic starts after the channel panel, truncated on the right */ 1228 int avail = W - CHAN_W - 1; 1229 if (avail < 1) avail = 1; 1230 char row[512]; 1231 if (ch->topic[0]) 1232 snprintf(row, sizeof(row), " %.*s", avail-1, ch->topic); 1233 else 1234 row[0]='\0'; 1235 int l=strlen(row); 1236 while (l < avail) row[l++]=' '; 1237 row[avail]='\0'; 1238 mvwaddnstr(win_status, 0, CHAN_W, row, avail); 1239 wnoutrefresh(win_status); 1240 } 1241 1242 static void draw_channels(void) { 1243 int H,W; getmaxyx(win_chan,H,W); (void)W; 1244 werase(win_chan); 1245 wattron(win_chan,COLOR_PAIR(C_BORDER)|A_BOLD); 1246 mvwaddnstr(win_chan,0,0," CHANNELS",CHAN_W-1); 1247 wattroff(win_chan,COLOR_PAIR(C_BORDER)|A_BOLD); 1248 1249 int row=2; 1250 for (int si=0; si<g_srv_count && row<H-1; si++) { 1251 /* server header: full hostname, ~ prefix if disconnected */ 1252 char hdr[CHAN_W]; 1253 const char *conn_mark = g_srv[si].connected ? "" : "~"; 1254 snprintf(hdr,sizeof(hdr),"%s%.*s",conn_mark,(int)(CHAN_W-2),g_srv[si].host); 1255 int hl=strlen(hdr); 1256 while (hl<CHAN_W-1) hdr[hl++]=' '; hdr[CHAN_W-1]='\0'; 1257 wattron(win_chan,COLOR_PAIR(C_SERVER_HDR)|A_BOLD); 1258 mvwaddnstr(win_chan,row,0,hdr,CHAN_W-1); 1259 wattroff(win_chan,COLOR_PAIR(C_SERVER_HDR)|A_BOLD); 1260 row++; 1261 1262 /* show *status* first, then regular channels */ 1263 for (int pass=0; pass<2 && row<H-1; pass++) { 1264 for (int ci=0; ci<g_chan_count && row<H-1; ci++) { 1265 if (g_chans[ci].srv!=si) continue; 1266 int is_status=(strcmp(g_chans[ci].name,"*status*")==0); 1267 if (pass==0 && !is_status) continue; /* pass 0: status only */ 1268 if (pass==1 && is_status) continue; /* pass 1: channels only */ 1269 Channel *ch=&g_chans[ci]; 1270 int attr; const char *pfx; 1271 if (ci==g_active) { 1272 attr=COLOR_PAIR(C_CHAN_SEL)|A_BOLD; pfx="> "; 1273 } else if (is_status) { 1274 attr=COLOR_PAIR(C_STATUS)|A_DIM; pfx=" "; 1275 } else if (ch->mention) { 1276 attr=COLOR_PAIR(C_MENTION_CHAN)|A_BOLD; pfx="! "; 1277 } else if (ch->unread) { 1278 attr=COLOR_PAIR(C_UNREAD); pfx="+ "; 1279 } else { 1280 attr=COLOR_PAIR(C_CHAN); pfx=" "; 1281 } 1282 char label[CHAN_W+3]; 1283 snprintf(label,sizeof(label)," %s%.*s",pfx,CHAN_W-4,ch->name); 1284 int ll=strlen(label); 1285 while (ll<CHAN_W-1) label[ll++]=' '; label[CHAN_W-1]='\0'; 1286 wattron(win_chan,attr); 1287 mvwaddnstr(win_chan,row,0,label,CHAN_W-1); 1288 wattroff(win_chan,attr); 1289 row++; 1290 } 1291 } 1292 } 1293 1294 /* bottom: active channel name */ 1295 char hint[CHAN_W]; 1296 snprintf(hint,sizeof(hint),"%.*s",CHAN_W-1,g_chans[g_active].name); 1297 int hl=strlen(hint); 1298 while (hl<CHAN_W-1) hint[hl++]=' '; hint[CHAN_W-1]='\0'; 1299 wattron(win_chan,COLOR_PAIR(C_STATUS)|A_DIM); 1300 mvwaddnstr(win_chan,H-1,0,hint,CHAN_W-1); 1301 wattroff(win_chan,COLOR_PAIR(C_STATUS)|A_DIM); 1302 1303 wnoutrefresh(win_chan); 1304 } 1305 1306 static void draw_chat(void) { 1307 int chat_h,chat_w; getmaxyx(win_chat,chat_h,chat_w); 1308 werase(win_chat); 1309 Channel *ch=&g_chans[g_active]; 1310 if (ch->line_count==0) { wnoutrefresh(win_chat); return; } 1311 1312 typedef struct { int flag; char text[MAX_LINE]; int has_url; } FlatLine; 1313 static FlatLine flat[MAX_HISTORY*4]; 1314 int flat_count=0; 1315 1316 for (int li=0; li<ch->line_count && flat_count<(int)(sizeof(flat)/sizeof(flat[0])); li++) { 1317 Line *ln=chan_line(ch,li); if (!ln) continue; 1318 char full[MAX_LINE]; 1319 snprintf(full,sizeof(full),"%s %s",ln->ts,ln->text); 1320 int plen=strlen(ln->ts)+1; 1321 int avail=chat_w-plen; if(avail<4)avail=4; 1322 const char *p=full; int is_first=1; 1323 while (*p && flat_count<(int)(sizeof(flat)/sizeof(flat[0]))) { 1324 int w=is_first?chat_w:avail; 1325 FlatLine *fl=&flat[flat_count++]; 1326 fl->flag=ln->flag; 1327 fl->has_url=(find_url(ln->text,0,&(int){0})>=0); 1328 if ((int)strlen(p)<=w) { 1329 if (!is_first) { memset(fl->text,' ',plen); strncpy(fl->text+plen,p,MAX_LINE-plen-1); } 1330 else strncpy(fl->text,p,MAX_LINE-1); 1331 break; 1332 } 1333 int cut=w; 1334 while (cut>0&&p[cut]!=' ') cut--; 1335 if (cut==0) cut=w; 1336 if (!is_first) { memset(fl->text,' ',plen); memcpy(fl->text+plen,p,cut); fl->text[plen+cut]='\0'; } 1337 else { memcpy(fl->text,p,cut); fl->text[cut]='\0'; } 1338 is_first=0; p+=cut; while(*p==' ')p++; 1339 } 1340 } 1341 1342 int max_scroll=flat_count>chat_h?flat_count-chat_h:0; 1343 if (ch->scroll>max_scroll) ch->scroll=max_scroll; 1344 if (ch->scroll<0) ch->scroll=0; 1345 int vis_start=flat_count-chat_h-ch->scroll; 1346 if (vis_start<0) vis_start=0; 1347 int vis_end=vis_start+chat_h; 1348 if (vis_end>flat_count) vis_end=flat_count; 1349 1350 for (int i=vis_start; i<vis_end; i++) { 1351 int row=i-vis_start; 1352 FlatLine *fl=&flat[i]; 1353 if (fl->flag==F_MSG) { 1354 wattron(win_chat,COLOR_PAIR(C_STATUS)); 1355 mvwaddnstr(win_chat,row,0,fl->text,chat_w-1); 1356 wattroff(win_chat,COLOR_PAIR(C_STATUS)); 1357 const char *lt=strchr(fl->text,'<'); 1358 const char *gt=lt?strchr(lt,'>'):NULL; 1359 if (lt&>) { 1360 int ns=(int)(lt-fl->text), nl=(int)(gt-lt+1); 1361 char nn[MAX_NICK]; int nlen=nl-2; if(nlen>=MAX_NICK)nlen=MAX_NICK-1; 1362 memcpy(nn,lt+1,nlen); nn[nlen]='\0'; 1363 mvwchgat(win_chat,row,ns,nl,A_BOLD,nick_colour(nn),NULL); 1364 } 1365 } else { 1366 int attr; 1367 switch(fl->flag) { 1368 case F_ME: attr=COLOR_PAIR(C_ME)|A_BOLD; break; 1369 case F_MENTION: attr=COLOR_PAIR(C_MENTION)|A_BOLD; break; 1370 case F_JOIN: attr=COLOR_PAIR(C_JOIN); break; 1371 case F_ACTION: attr=COLOR_PAIR(C_ACTION)|A_ITALIC; break; 1372 default: attr=COLOR_PAIR(C_STATUS); break; 1373 } 1374 wattron(win_chat,attr); 1375 mvwaddnstr(win_chat,row,0,fl->text,chat_w-1); 1376 wattroff(win_chat,attr); 1377 } 1378 if (fl->has_url) { 1379 int ustart=0,ulen,us; 1380 while ((us=find_url(fl->text,ustart,&ulen))>=0) { 1381 if (us>=chat_w-1) break; 1382 int ue=us+ulen; if(ue>chat_w-2)ue=chat_w-2; 1383 mvwchgat(win_chat,row,us,ue-us,A_UNDERLINE,C_URL,NULL); 1384 ustart=us+ulen; 1385 } 1386 } 1387 } 1388 1389 if (ch->scroll>0) { 1390 char tag[16]; snprintf(tag,sizeof(tag),"^%d",ch->scroll); 1391 wattron(win_chat,COLOR_PAIR(C_STATUS)|A_DIM); 1392 mvwaddstr(win_chat,0,chat_w-(int)strlen(tag)-1,tag); 1393 wattroff(win_chat,COLOR_PAIR(C_STATUS)|A_DIM); 1394 } 1395 wnoutrefresh(win_chat); 1396 } 1397 1398 static void draw_users(void) { 1399 int H,W; getmaxyx(win_users,H,W); (void)W; 1400 werase(win_users); 1401 Channel *ch=&g_chans[g_active]; 1402 char hdr[32]; snprintf(hdr,sizeof(hdr)," USERS (%d)",ch->user_count); 1403 wattron(win_users,COLOR_PAIR(C_BORDER)|A_BOLD); 1404 mvwaddnstr(win_users,0,1,hdr,USER_W-2); 1405 wattroff(win_users,COLOR_PAIR(C_BORDER)|A_BOLD); 1406 for (int i=0; i<ch->user_count; i++) { 1407 int y=i+2; if(y>=H-1) break; 1408 const char *entry = ch->users[i]; 1409 char mode = entry[0]; 1410 const char *bare = entry+1; /* nick without prefix */ 1411 /* colour the mode symbol */ 1412 if (mode=='@'||mode=='~'||mode=='&') { 1413 wattron(win_users, COLOR_PAIR(C_MENTION)|A_BOLD); 1414 mvwaddnstr(win_users,y,1,&mode,1); 1415 wattroff(win_users, COLOR_PAIR(C_MENTION)|A_BOLD); 1416 } else if (mode=='+'||mode=='%') { 1417 wattron(win_users, COLOR_PAIR(C_ME)|A_BOLD); 1418 mvwaddnstr(win_users,y,1,&mode,1); 1419 wattroff(win_users, COLOR_PAIR(C_ME)|A_BOLD); 1420 } else { 1421 mvwaddch(win_users,y,1,' '); 1422 } 1423 /* nick in normal white */ 1424 wattron(win_users,COLOR_PAIR(C_CHAN)); 1425 mvwaddnstr(win_users,y,2,bare,USER_W-3); 1426 wattroff(win_users,COLOR_PAIR(C_CHAN)); 1427 } 1428 wnoutrefresh(win_users); 1429 } 1430 1431 static void draw_input(void) { 1432 int iw; getmaxyx(win_input,(int){0},iw); 1433 werase(win_input); 1434 wbkgd(win_input,COLOR_PAIR(C_INPUT)); 1435 1436 /* draw "> " prompt */ 1437 wattron(win_input, COLOR_PAIR(C_BORDER)|A_BOLD); 1438 mvwaddstr(win_input, 0, 0, "> "); 1439 wattroff(win_input, COLOR_PAIR(C_BORDER)|A_BOLD); 1440 1441 /* available width after prompt */ 1442 int pfx = 2; 1443 int avail = iw - pfx - 1; 1444 if (avail < 1) avail = 1; 1445 1446 /* scroll text so cursor stays visible */ 1447 int disp = 0, cx = g_input_cur; 1448 if (g_input_cur > avail) { 1449 disp = g_input_cur - avail; 1450 cx = avail; 1451 } 1452 1453 wattron(win_input, COLOR_PAIR(C_INPUT)); 1454 mvwaddnstr(win_input, 0, pfx, g_input + disp, avail); 1455 wattroff(win_input, COLOR_PAIR(C_INPUT)); 1456 1457 /* place terminal cursor — curs_set(2) gives blinking block on most terms */ 1458 wmove(win_input, 0, pfx + cx); 1459 wnoutrefresh(win_input); 1460 } 1461 1462 static void draw_all(void) { 1463 draw_status_bar(); draw_channels(); draw_chat(); 1464 draw_users(); draw_input(); doupdate(); 1465 } 1466 1467 /* ── window setup ──────────────────────────────────────────────────────────── */ 1468 1469 static void init_colors(void) { 1470 start_color(); use_default_colors(); 1471 init_pair(C_BORDER, COLOR_CYAN, -1); 1472 init_pair(C_HEADER, COLOR_BLACK, COLOR_CYAN); 1473 init_pair(C_ME, COLOR_YELLOW, -1); 1474 init_pair(C_JOIN, COLOR_BLACK+8, -1); 1475 init_pair(C_MENTION, COLOR_RED, -1); 1476 init_pair(C_STATUS, COLOR_WHITE, -1); 1477 init_pair(C_INPUT, COLOR_WHITE, COLOR_BLACK); 1478 init_pair(C_CHAN_SEL, COLOR_BLACK, COLOR_WHITE); 1479 init_pair(C_CHAN, COLOR_WHITE, -1); 1480 init_pair(C_UNREAD, COLOR_YELLOW, -1); 1481 init_pair(C_MENTION_CHAN,COLOR_RED, -1); 1482 init_pair(C_URL, COLOR_BLUE, -1); 1483 init_pair(C_ACTION, COLOR_MAGENTA, -1); 1484 init_pair(C_SERVER_HDR, COLOR_CYAN, -1); 1485 static const int nick_cols[C_NICK_COUNT]={ 1486 COLOR_GREEN,COLOR_CYAN,COLOR_MAGENTA,COLOR_YELLOW, 1487 COLOR_WHITE,COLOR_RED, COLOR_BLUE, COLOR_GREEN 1488 }; 1489 for (int i=0;i<C_NICK_COUNT;i++) 1490 init_pair(C_NICK_BASE+i,nick_cols[i],-1); 1491 } 1492 1493 static void build_windows(void) { 1494 int H,W; getmaxyx(stdscr,H,W); 1495 int chat_w=W-CHAN_W-USER_W, chat_h=H-3; 1496 if(win_chan) delwin(win_chan); 1497 if(win_chat) delwin(win_chat); 1498 if(win_users) delwin(win_users); 1499 if(win_input) delwin(win_input); 1500 if(win_status) delwin(win_status); 1501 win_status=newwin(1, W, 0, 0); 1502 win_chan =newwin(H, CHAN_W, 0, 0); 1503 win_chat =newwin(chat_h,chat_w,1, CHAN_W); 1504 win_users =newwin(H, USER_W,0, CHAN_W+chat_w); 1505 win_input =newwin(1, chat_w,H-1,CHAN_W); 1506 } 1507 1508 /* ── event handler ─────────────────────────────────────────────────────────── */ 1509 1510 static void do_rejoin_channels(int si) { 1511 /* always join every configured autojoin channel */ 1512 for (int j = 0; j < g_srv[si].autojoin_count; j++) 1513 srv_sendf(si, "JOIN %s", g_srv[si].autojoin[j]); 1514 1515 /* also rejoin any channels open from a previous session that aren't 1516 in the autojoin list (e.g. channels joined manually before disconnect) */ 1517 for (int i = 0; i < g_chan_count; i++) { 1518 if (g_chans[i].srv != si || g_chans[i].name[0] != '#') continue; 1519 int already = 0; 1520 for (int j = 0; j < g_srv[si].autojoin_count; j++) 1521 if (strcasecmp(g_srv[si].autojoin[j], g_chans[i].name) == 0) 1522 { already = 1; break; } 1523 if (!already) 1524 srv_sendf(si, "JOIN %s", g_chans[i].name); 1525 } 1526 } 1527 1528 static void handle_event(const Event *ev) { 1529 int si=ev->srv; 1530 Server *s=&g_srv[si]; 1531 1532 switch (ev->type) { 1533 1534 case EV_CONNECTED: 1535 srv_status_msg(si,"Connected."); 1536 do_rejoin_channels(si); 1537 break; 1538 1539 case EV_STATUS: 1540 srv_status_msg(si,ev->text); 1541 break; 1542 1543 case EV_ERROR: 1544 srv_status_fmt(si,"[ERR] %s",ev->text); 1545 break; 1546 1547 case EV_RECONNECT: 1548 if (!s->connected) schedule_reconnect(si); 1549 break; 1550 1551 case EV_SERVER_TEXT: 1552 if (ev->chan[0]) { 1553 int ci=chan_find(si,ev->chan); 1554 if (ci<0) ci=chan_add(si,ev->chan); 1555 chan_addline(&g_chans[ci],F_STATUS,ev->text); 1556 if (ci==g_active) g_chans[ci].unread=0; 1557 } else { 1558 srv_status_msg(si,ev->text); 1559 } 1560 break; 1561 1562 case EV_RAW: 1563 srv_status_msg(si,ev->text); 1564 break; 1565 1566 case EV_PRIVMSG: { 1567 if (is_ignored(ev->nick)) break; 1568 const char *target=ev->chan, *text=ev->text; 1569 const char *dest=(target[0]=='#')?target:ev->nick; 1570 int ci=chan_find(si,dest); if(ci<0) ci=chan_add(si,dest); 1571 1572 if (text[0]=='\x01'&&strncmp(text+1,"ACTION",6)==0) { 1573 const char *act=text+8; 1574 char msg[MAX_LINE]; 1575 int alen=strlen(act); if(alen>0&&act[alen-1]=='\x01')alen--; 1576 snprintf(msg,sizeof(msg),"* %s %.*s",ev->nick,alen,act); 1577 int flag=(strcasestr(msg,s->nick)!=NULL)?F_MENTION:F_ACTION; 1578 chan_addline(&g_chans[ci],flag,msg); 1579 if(ci!=g_active&&flag==F_MENTION) g_chans[ci].mention=1; 1580 if(ci==g_active) g_chans[ci].unread=0; 1581 break; 1582 } 1583 char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<%s> %s",ev->nick,text); 1584 int flag; 1585 if (strcmp(ev->nick,s->nick)==0) flag=F_ME; 1586 else if (strcasestr(text,s->nick)!=NULL) flag=F_MENTION; 1587 else flag=F_MSG; 1588 chan_addline(&g_chans[ci],flag,msg); 1589 if(ci!=g_active&&flag==F_MENTION) g_chans[ci].mention=1; 1590 if(ci==g_active) g_chans[ci].unread=0; 1591 break; 1592 } 1593 1594 case EV_JOIN: { 1595 int ci=chan_find(si,ev->chan); if(ci<0) ci=chan_add(si,ev->chan); 1596 if (strcmp(ev->nick,s->nick)==0) { 1597 g_chans[ci].user_count=0; 1598 g_active=ci; 1599 g_chans[ci].unread=0; g_chans[ci].mention=0; 1600 srv_status_fmt(si,"Joined %s",ev->chan); 1601 } else { 1602 char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"-> %s joined",ev->nick); 1603 chan_addline(&g_chans[ci],F_JOIN,msg); 1604 chan_adduser(&g_chans[ci], 0, ev->nick); 1605 } 1606 break; 1607 } 1608 1609 case EV_PART: { 1610 int ci=chan_find(si,ev->chan); if(ci<0) break; 1611 char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<- %s parted (%s)",ev->nick,ev->text); 1612 chan_addline(&g_chans[ci],F_JOIN,msg); 1613 if (strcmp(ev->nick,s->nick)==0) chan_remove(ci); 1614 else chan_removeuser(&g_chans[ci],ev->nick); 1615 break; 1616 } 1617 1618 case EV_QUIT_MSG: 1619 for (int i=0;i<g_chan_count;i++) { 1620 if (g_chans[i].srv!=si) continue; 1621 int found=0; 1622 for (int j=0;j<g_chans[i].user_count;j++) 1623 if (strcasecmp(g_chans[i].users[j],ev->nick)==0){found=1;break;} 1624 if (found) { 1625 char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<- %s quit (%s)",ev->nick,ev->text); 1626 chan_addline(&g_chans[i],F_JOIN,msg); 1627 chan_removeuser(&g_chans[i],ev->nick); 1628 } 1629 } 1630 break; 1631 1632 case EV_NICK_CHANGE: 1633 if (strcmp(ev->nick,s->nick)==0) { 1634 strncpy(s->nick,ev->extra,MAX_NICK-1); 1635 srv_status_fmt(si,"You are now known as %s",s->nick); 1636 } 1637 for (int i=0;i<g_chan_count;i++) { 1638 if (g_chans[i].srv!=si) continue; 1639 for (int j=0;j<g_chans[i].user_count;j++) { 1640 /* skip stored mode prefix when comparing */ 1641 const char *sn=g_chans[i].users[j]; 1642 if (*sn=='@'||*sn=='+'||*sn=='~'||*sn=='&'||*sn=='%'||*sn==' ') sn++; 1643 if (strcasecmp(sn,ev->nick)==0) { 1644 char mode=g_chans[i].users[j][0]; 1645 g_chans[i].users[j][0]=mode; 1646 strncpy(g_chans[i].users[j]+1,ev->extra,MAX_NICK-2); 1647 char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"~ %s is now %s",ev->nick,ev->extra); 1648 chan_addline(&g_chans[i],F_JOIN,msg); 1649 break; 1650 } 1651 } 1652 } 1653 break; 1654 1655 case EV_NAMES: { 1656 int ci=chan_find(si,ev->chan); if(ci<0) break; 1657 /* first nick of a fresh NAMES reply — wipe the stale list */ 1658 if (!g_chans[ci].names_pending) { 1659 g_chans[ci].user_count = 0; 1660 g_chans[ci].names_pending = 1; 1661 } 1662 chan_adduser(&g_chans[ci], ev->extra[0], ev->nick); 1663 chan_sort_users(&g_chans[ci]); 1664 break; 1665 } 1666 1667 case EV_KICK: { 1668 int ci=chan_find(si,ev->chan); if(ci<0) break; 1669 char msg[MAX_LINE]; 1670 snprintf(msg,sizeof(msg),"X %s was kicked by %s (%s)",ev->extra,ev->nick,ev->text); 1671 chan_addline(&g_chans[ci],F_JOIN,msg); 1672 chan_removeuser(&g_chans[ci],ev->extra); 1673 break; 1674 } 1675 1676 case EV_TOPIC: { 1677 int ci=chan_find(si,ev->chan); if(ci<0) break; 1678 strip_irc_fmt(ev->text, g_chans[ci].topic, MAX_LINE); 1679 break; 1680 } 1681 1682 case EV_NAMES_END: { 1683 int ci=chan_find(si,ev->chan); if(ci<0) break; 1684 g_chans[ci].names_pending = 0; 1685 break; 1686 } 1687 1688 } 1689 } 1690 1691 /* ── tab completion ────────────────────────────────────────────────────────── */ 1692 1693 static void tab_reset(void) { g_tab_active=0; g_tab_count=0; g_tab_idx=0; } 1694 1695 static void tab_complete(void) { 1696 if (g_active<0||g_active>=g_chan_count) return; 1697 Channel *ch=&g_chans[g_active]; 1698 if (!g_tab_active) { 1699 int ws=g_input_cur; 1700 while (ws>0&&g_input[ws-1]!=' '&&g_input[ws-1]!='\t') ws--; 1701 char prefix[MAX_NICK]; int plen=g_input_cur-ws; 1702 if (plen<=0) return; 1703 strncpy(prefix,g_input+ws,plen); prefix[plen]='\0'; 1704 char *pp=prefix; 1705 while(*pp=='@'||*pp=='+'||*pp=='~'||*pp=='&'||*pp=='%') pp++; 1706 if (!*pp) return; 1707 g_tab_count=0; 1708 for (int i=0;i<ch->user_count;i++) { 1709 const char *bare=ch->users[i]+1; /* skip mode prefix */ 1710 if (str_icase_starts(bare,pp)) 1711 strncpy(g_tab_matches[g_tab_count++],bare,MAX_NICK-1); 1712 } 1713 if (!g_tab_count) return; 1714 g_tab_idx=0; g_tab_word_start=ws; g_tab_active=1; 1715 } else { 1716 g_tab_idx=(g_tab_idx+1)%g_tab_count; 1717 } 1718 const char *nick=g_tab_matches[g_tab_idx]; 1719 char repl[MAX_NICK+3]; 1720 snprintf(repl,sizeof(repl),g_tab_word_start==0?"%s: ":"%s ",nick); 1721 int rlen=strlen(repl); 1722 char newbuf[MAX_INPUT]; 1723 int tail=g_input_len-g_input_cur; if(tail<0)tail=0; 1724 memcpy(newbuf,g_input,g_tab_word_start); 1725 memcpy(newbuf+g_tab_word_start,repl,rlen); 1726 memcpy(newbuf+g_tab_word_start+rlen,g_input+g_input_cur,tail); 1727 int newlen=g_tab_word_start+rlen+tail; 1728 if(newlen>=MAX_INPUT)newlen=MAX_INPUT-1; 1729 memcpy(g_input,newbuf,newlen); g_input[newlen]='\0'; 1730 g_input_len=newlen; g_input_cur=g_tab_word_start+rlen; 1731 } 1732 1733 /* ── command processing ────────────────────────────────────────────────────── */ 1734 1735 /* current server index = server of the active channel */ 1736 static int active_srv(void) { 1737 if (g_active>=0&&g_active<g_chan_count) return g_chans[g_active].srv; 1738 return 0; 1739 } 1740 1741 static void process_input(const char *text) { 1742 int si=active_srv(); 1743 Server *s=&g_srv[si]; 1744 1745 if (text[0]!='/') { 1746 if (g_active<0) { srv_status_msg(si,"Use /join #channel first."); return; } 1747 const char *cn=g_chans[g_active].name; 1748 if (cn[0]=='*') { srv_status_msg(si,"Use /join #channel first."); return; } 1749 if (!s->connected) { srv_status_msg(si,"[not connected]"); return; } 1750 srv_sendf(si,"PRIVMSG %s :%s",cn,text); 1751 char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"<%s> %s",s->nick,text); 1752 chan_addline(&g_chans[g_active],F_ME,msg); 1753 g_chans[g_active].unread=0; 1754 return; 1755 } 1756 1757 char cmd[64],arg[MAX_INPUT]; 1758 cmd[0]='\0'; arg[0]='\0'; 1759 const char *sp=strchr(text+1,' '); 1760 if (sp) { 1761 int cl=(int)(sp-text); if(cl>=(int)sizeof(cmd))cl=sizeof(cmd)-1; 1762 memcpy(cmd,text,cl); cmd[cl]='\0'; 1763 const char *a=sp+1; while(*a==' ')a++; 1764 strncpy(arg,a,MAX_INPUT-1); 1765 } else strncpy(cmd,text,sizeof(cmd)-1); 1766 for (char *c=cmd;*c;c++) *c=tolower((unsigned char)*c); 1767 1768 if (strcmp(cmd,"/join")==0) { 1769 char ch[MAX_CHAN]; strncpy(ch,arg[0]?arg:g_srv[si].autojoin_count?g_srv[si].autojoin[0]:"#general",MAX_CHAN-1); 1770 if(ch[0]!='#'){memmove(ch+1,ch,strlen(ch)+1);ch[0]='#';} 1771 chan_add(si,ch); 1772 if(s->connected) srv_sendf(si,"JOIN %s",ch); 1773 else srv_status_msg(si,"[not connected]"); 1774 1775 } else if (strcmp(cmd,"/part")==0) { 1776 const char *ch=arg[0]?arg:g_chans[g_active].name; 1777 if(s->connected) srv_sendf(si,"PART %s :bye",ch); 1778 else srv_status_msg(si,"[not connected]"); 1779 1780 } else if (strcmp(cmd,"/cycle")==0) { 1781 const char *ch=arg[0]?arg:g_chans[g_active].name; 1782 if(ch[0]!='#'){srv_status_msg(si,"Not in a channel.");return;} 1783 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1784 srv_sendf(si,"PART %s :cycling",ch); srv_sendf(si,"JOIN %s",ch); 1785 1786 } else if (strcmp(cmd,"/nick")==0) { 1787 if(!arg[0]) { 1788 srv_status_fmt(si,"Your nick is: %s",s->nick); 1789 } else { 1790 if(s->connected) srv_sendf(si,"NICK %s",arg); 1791 else strncpy(s->nick,arg,MAX_NICK-1); 1792 srv_status_fmt(si,"Nick change requested: %s -> %s",s->nick,arg); 1793 } 1794 1795 } else if (strcmp(cmd,"/msg")==0) { 1796 char tgt[MAX_NICK],msg[MAX_LINE]; 1797 if(sscanf(arg,"%63s %[^\n]",tgt,msg)==2) { 1798 chan_add(si,tgt); 1799 if(s->connected) srv_sendf(si,"PRIVMSG %s :%s",tgt,msg); 1800 int ci=chan_find(si,tgt); 1801 if(ci>=0){ char full[MAX_LINE]; snprintf(full,sizeof(full),"<%s> %s",s->nick,msg); chan_addline(&g_chans[ci],F_ME,full); } 1802 } 1803 1804 } else if (strcmp(cmd,"/me")==0) { 1805 const char *cn=g_chans[g_active].name; 1806 if(cn[0]=='*'){srv_status_msg(si,"Not in a channel.");return;} 1807 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1808 srv_sendf(si,"PRIVMSG %s :\x01""ACTION %s\x01",cn,arg); 1809 char msg[MAX_LINE]; snprintf(msg,sizeof(msg),"* %s %s",s->nick,arg); 1810 chan_addline(&g_chans[g_active],F_ACTION,msg); 1811 1812 } else if (strcmp(cmd,"/notice")==0) { 1813 char tgt[MAX_NICK],msg[MAX_LINE]; 1814 if(sscanf(arg,"%63s %[^\n]",tgt,msg)==2) { 1815 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1816 srv_sendf(si,"NOTICE %s :%s",tgt,msg); 1817 srv_status_fmt(si,"->%s<- %s",tgt,msg); 1818 } else srv_status_msg(si,"Usage: /notice <target> <message>"); 1819 1820 } else if (strcmp(cmd,"/ctcp")==0) { 1821 char tgt[MAX_NICK],ctcp[MAX_LINE]; 1822 if(sscanf(arg,"%63s %[^\n]",tgt,ctcp)>=1) { 1823 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1824 for(char *c=ctcp;*c&&*c!=' ';c++) *c=toupper((unsigned char)*c); 1825 srv_sendf(si,"PRIVMSG %s :\x01%s\x01",tgt,ctcp); 1826 srv_status_fmt(si,"[ctcp] %s -> %s",ctcp,tgt); 1827 } else srv_status_msg(si,"Usage: /ctcp <nick> <command>"); 1828 1829 } else if (strcmp(cmd,"/names")==0) { 1830 const char *ch=arg[0]?arg:g_chans[g_active].name; 1831 if(ch[0]=='*'){srv_status_msg(si,"Not in a channel.");return;} 1832 if(s->connected) srv_sendf(si,"NAMES %s",ch); 1833 else { 1834 int ci=chan_find(si,ch); if(ci<0){srv_status_msg(si,"Channel not found.");return;} 1835 srv_status_fmt(si,"[names] %s (%d users):",ch,g_chans[ci].user_count); 1836 char line[MAX_LINE]; line[0]='\0'; 1837 for(int i=0;i<g_chans[ci].user_count;i++){ 1838 if(strlen(line)+strlen(g_chans[ci].users[i])+2>60){ 1839 srv_status_fmt(si,"[names] %s",line); line[0]='\0'; 1840 } 1841 if(line[0]) strncat(line," ",MAX_LINE-1); 1842 strncat(line,g_chans[ci].users[i],MAX_LINE-1); 1843 } 1844 if(line[0]) srv_status_fmt(si,"[names] %s",line); 1845 } 1846 1847 } else if (strcmp(cmd,"/topic")==0) { 1848 const char *ch=g_chans[g_active].name; 1849 if(ch[0]=='*'){srv_status_msg(si,"Not in a channel.");return;} 1850 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1851 if(arg[0]) srv_sendf(si,"TOPIC %s :%s",ch,arg); 1852 else srv_sendf(si,"TOPIC %s",ch); 1853 1854 } else if (strcmp(cmd,"/whois")==0) { 1855 if(!arg[0]){srv_status_msg(si,"Usage: /whois <nick>");return;} 1856 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1857 srv_sendf(si,"WHOIS %s",arg); 1858 1859 } else if (strcmp(cmd,"/who")==0) { 1860 const char *tgt=arg[0]?arg:g_chans[g_active].name; 1861 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1862 srv_sendf(si,"WHO %s",tgt); 1863 1864 } else if (strcmp(cmd,"/list")==0) { 1865 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1866 srv_status_msg(si,"[list] Requesting channel list..."); 1867 if(arg[0]) srv_sendf(si,"LIST %s",arg); else srv_send_raw(si,"LIST"); 1868 1869 } else if (strcmp(cmd,"/away")==0) { 1870 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1871 if(arg[0]) srv_sendf(si,"AWAY :%s",arg); else srv_send_raw(si,"AWAY"); 1872 1873 } else if (strcmp(cmd,"/back")==0) { 1874 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1875 srv_send_raw(si,"AWAY"); 1876 1877 } else if (strcmp(cmd,"/invite")==0) { 1878 char tgt[MAX_NICK],ch[MAX_CHAN]; ch[0]='\0'; 1879 if(sscanf(arg,"%63s %63s",tgt,ch)<1){srv_status_msg(si,"Usage: /invite <nick> [#chan]");return;} 1880 if(!ch[0]) strncpy(ch,g_chans[g_active].name,MAX_CHAN-1); 1881 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1882 srv_sendf(si,"INVITE %s %s",tgt,ch); 1883 1884 } else if (strcmp(cmd,"/kick")==0) { 1885 char tgt[MAX_NICK],reason[MAX_LINE]; reason[0]='\0'; 1886 if(sscanf(arg,"%63s %[^\n]",tgt,reason)<1){srv_status_msg(si,"Usage: /kick <nick> [reason]");return;} 1887 const char *ch=g_chans[g_active].name; 1888 if(ch[0]!='#'){srv_status_msg(si,"Not in a channel.");return;} 1889 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1890 if(reason[0]) srv_sendf(si,"KICK %s %s :%s",ch,tgt,reason); 1891 else srv_sendf(si,"KICK %s %s",ch,tgt); 1892 1893 } else if (strcmp(cmd,"/mode")==0) { 1894 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1895 if(!arg[0]) srv_sendf(si,"MODE %s",g_chans[g_active].name); 1896 else srv_sendf(si,"MODE %s",arg); 1897 1898 } else if (strcmp(cmd,"/raw")==0||strcmp(cmd,"/quote")==0) { 1899 if(!arg[0]){srv_status_msg(si,"Usage: /raw <irc line>");return;} 1900 if(!s->connected){srv_status_msg(si,"[not connected]");return;} 1901 srv_send_raw(si,arg); 1902 srv_status_fmt(si,">> %s",arg); 1903 1904 } else if (strcmp(cmd,"/server")==0) { 1905 /* /server host [port] — add a new server connection */ 1906 char newhost[MAX_HOST]; int newport=6697; 1907 if(sscanf(arg,"%255s %d",newhost,&newport)<1){srv_status_msg(si,"Usage: /server <host> [port]");return;} 1908 int nsi=srv_alloc(); 1909 if(nsi<0){srv_status_msg(si,"Max servers reached.");return;} 1910 strncpy(g_srv[nsi].host,newhost,MAX_HOST-1); 1911 g_srv[nsi].port=newport; 1912 /* inherit nick from current server */ 1913 strncpy(g_srv[nsi].nick,s->nick,MAX_NICK-1); 1914 chan_add(nsi,"*status*"); 1915 start_net_thread(nsi); 1916 srv_status_fmt(nsi,"Connecting to %s:%d...",newhost,newport); 1917 1918 } else if (strcmp(cmd,"/connect")==0) { 1919 g_srv[si].net_stop=1; 1920 start_net_thread(si); 1921 1922 } else if (strcmp(cmd,"/ignore")==0) { 1923 if(!arg[0]) { 1924 if(!g_ignore_count){srv_status_msg(si,"Ignore list is empty.");return;} 1925 srv_status_msg(si,"Ignored nicks:"); 1926 for(int i=0;i<g_ignore_count;i++) srv_status_fmt(si," %s",g_ignore[i]); 1927 return; 1928 } 1929 ignore_add(arg); srv_status_fmt(si,"Ignoring %s",arg); 1930 1931 } else if (strcmp(cmd,"/unignore")==0) { 1932 if(is_ignored(arg)){ignore_remove(arg);srv_status_fmt(si,"Unignored %s",arg);} 1933 else srv_status_fmt(si,"%s is not ignored",arg); 1934 1935 } else if (strcmp(cmd,"/clear")==0) { 1936 g_chans[g_active].line_count=0; 1937 g_chans[g_active].line_head=0; 1938 g_chans[g_active].scroll=0; 1939 1940 } else if (strcmp(cmd,"/quit")==0) { 1941 for (int i=0;i<g_srv_count;i++) 1942 if(g_srv[i].connected) srv_sendf(i,"QUIT :%s",arg[0]?arg:"Goodbye"); 1943 endwin(); exit(0); 1944 1945 } else if (strcmp(cmd,"/help")==0) { 1946 static const char *h[]={ 1947 "Commands:", 1948 " /join #chan join channel", 1949 " /part [#chan] leave channel", 1950 " /cycle [#chan] part and rejoin", 1951 " /nick NAME change nick", 1952 " /msg NICK TEXT private message", 1953 " /me TEXT action message", 1954 " /notice TGT TEXT send notice", 1955 " /ctcp NICK CMD CTCP request", 1956 " /names [#chan] list users", 1957 " /topic [text] get/set topic", 1958 " /whois NICK whois lookup", 1959 " /who [target] who query", 1960 " /list [pattern] list channels", 1961 " /away [msg] set away", 1962 " /back clear away", 1963 " /invite NICK [chan] invite to channel", 1964 " /kick NICK [reason] kick from channel", 1965 " /mode [target] [m] get/set mode", 1966 " /server HOST [port] connect to new server", 1967 " /raw <line> send raw IRC line", 1968 " /ignore [nick] ignore (no arg = list)", 1969 " /unignore NICK unignore nick", 1970 " /clear clear scrollback", 1971 " /connect reconnect current server", 1972 " /quit [msg] quit", 1973 "Keys:", 1974 " Tab nick completion", 1975 " Ctrl+N/P next/prev channel", 1976 " PgUp/PgDn scroll chat", 1977 " Ctrl+W delete word left", 1978 " Up/Down input history", 1979 NULL 1980 }; 1981 for(int i=0;h[i];i++) srv_status_msg(si,h[i]); 1982 1983 } else { 1984 srv_status_fmt(si,"Unknown command: %s (/help for list)",cmd); 1985 } 1986 } 1987 1988 /* ── history ───────────────────────────────────────────────────────────────── */ 1989 1990 static void hist_push(const char *text) { 1991 if(g_hist_count<MAX_HIST_LINES) strncpy(g_hist[g_hist_count++].text,text,MAX_INPUT-1); 1992 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); } 1993 } 1994 1995 /* ── key handler ───────────────────────────────────────────────────────────── */ 1996 1997 static void handle_key(int key) { 1998 if (key==KEY_RESIZE) { clear(); build_windows(); return; } 1999 if (key=='\t') { tab_complete(); return; } 2000 tab_reset(); 2001 2002 Channel *ch=&g_chans[g_active]; 2003 2004 if (key==14) { g_active=chan_next(g_active,1); g_chans[g_active].unread=0; g_chans[g_active].mention=0; return; } 2005 if (key==16) { g_active=chan_next(g_active,-1); g_chans[g_active].unread=0; g_chans[g_active].mention=0; return; } 2006 2007 if (key==KEY_PPAGE) { int ch2; getmaxyx(win_chat,ch2,(int){0}); ch->scroll+=ch2/2; return; } 2008 if (key==KEY_NPAGE) { int ch2; getmaxyx(win_chat,ch2,(int){0}); ch->scroll-=ch2/2; if(ch->scroll<0)ch->scroll=0; return; } 2009 2010 if (key=='\n'||key=='\r'||key==KEY_ENTER) { 2011 if(g_input_len>0) { 2012 g_input[g_input_len]='\0'; 2013 char *p=g_input; while(*p==' ')p++; 2014 char *e=g_input+g_input_len-1; while(e>p&&*e==' ')*e--='\0'; 2015 if(*p) { hist_push(p); g_hist_pos=-1; process_input(p); } 2016 } 2017 g_input[0]='\0'; g_input_len=0; g_input_cur=0; return; 2018 } 2019 2020 if (key==127||key==KEY_BACKSPACE||key==8) { 2021 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--; } 2022 return; 2023 } 2024 2025 if (key==23) { /* Ctrl+W */ 2026 int cur=g_input_cur; 2027 while(cur>0&&g_input[cur-1]==' ') cur--; 2028 while(cur>0&&g_input[cur-1]!=' ') cur--; 2029 int del=g_input_cur-cur; 2030 memmove(g_input+cur,g_input+g_input_cur,g_input_len-g_input_cur+1); 2031 g_input_len-=del; g_input_cur=cur; return; 2032 } 2033 2034 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; } 2035 2036 if (key==KEY_LEFT &&g_input_cur>0) { g_input_cur--; return; } 2037 if (key==KEY_RIGHT &&g_input_cur<g_input_len) { g_input_cur++; return; } 2038 if (key==KEY_HOME) { g_input_cur=0; return; } 2039 if (key==KEY_END) { g_input_cur=g_input_len; return; } 2040 2041 if (key==KEY_UP) { 2042 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; } 2043 return; 2044 } 2045 if (key==KEY_DOWN) { 2046 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; } 2047 else { g_hist_pos=-1; g_input[0]='\0'; g_input_len=0; g_input_cur=0; } 2048 return; 2049 } 2050 2051 if (key>=32&&key<256) { 2052 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++; } 2053 return; 2054 } 2055 } 2056 2057 /* ── main ──────────────────────────────────────────────────────────────────── */ 2058 2059 static void usage(const char *prog) { 2060 fprintf(stderr, 2061 "Usage: %s [options]\n" 2062 " --host HOST server hostname\n" 2063 " --port PORT port (default 6697)\n" 2064 " --nick NICK nickname\n" 2065 " --channel CHAN channel(s) to join, comma-separated\n" 2066 " --tls / --no-tls TLS on/off (default on)\n" 2067 " --sasl-user USER SASL username\n" 2068 " --sasl-pass PASS SASL password\n" 2069 " --config FILE config file\n" 2070 "\n" 2071 "Multiple servers via ~/.sirc [server] blocks.\n", prog); 2072 } 2073 2074 int main(int argc, char **argv) { 2075 const char *config_path=NULL; 2076 for(int i=1;i<argc-1;i++) if(strcmp(argv[i],"--config")==0){config_path=argv[i+1];break;} 2077 load_config(config_path); 2078 2079 /* CLI server (only if --host given, or if no servers loaded yet) */ 2080 const char *cli_host=NULL; 2081 int cli_port=-1; int cli_tls=-1; 2082 const char *cli_nick=NULL, *cli_chan=NULL, *cli_su=NULL, *cli_sp=NULL; 2083 for(int i=1;i<argc;i++) { 2084 if (strcmp(argv[i],"--host")==0 &&i+1<argc) cli_host=argv[++i]; 2085 else if (strcmp(argv[i],"--port")==0 &&i+1<argc) cli_port=atoi(argv[++i]); 2086 else if (strcmp(argv[i],"--nick")==0 &&i+1<argc) cli_nick=argv[++i]; 2087 else if (strcmp(argv[i],"--channel")==0 &&i+1<argc) cli_chan=argv[++i]; 2088 else if (strcmp(argv[i],"--tls")==0) cli_tls=1; 2089 else if (strcmp(argv[i],"--no-tls")==0) cli_tls=0; 2090 else if (strcmp(argv[i],"--sasl-user")==0&&i+1<argc) cli_su=argv[++i]; 2091 else if (strcmp(argv[i],"--sasl-pass")==0&&i+1<argc) cli_sp=argv[++i]; 2092 else if (strcmp(argv[i],"--config")==0) i++; 2093 else if (strcmp(argv[i],"--help")==0) { usage(argv[0]); return 0; } 2094 } 2095 2096 /* if CLI gave a host, either override first server or add new one */ 2097 if (cli_host) { 2098 int si; 2099 if (g_srv_count==0) si=srv_alloc(); 2100 else si=0; 2101 strncpy(g_srv[si].host,cli_host,MAX_HOST-1); 2102 if(cli_port>0) g_srv[si].port=cli_port; 2103 if(cli_tls>=0) g_srv[si].use_tls=cli_tls; 2104 if(cli_chan) srv_add_autojoin(si,cli_chan); 2105 if(cli_su) strncpy(g_srv[si].sasl_user,cli_su,MAX_NICK-1); 2106 if(cli_sp) strncpy(g_srv[si].sasl_pass,cli_sp,255); 2107 if(cli_nick) strncpy(g_srv[si].nick,cli_nick,MAX_NICK-1); 2108 } else if (g_srv_count==0) { 2109 /* no config, no --host: add default server */ 2110 srv_alloc(); 2111 } 2112 2113 /* apply --nick / --channel / --tls to all servers that don't have 2114 their own value set (covers the no-host case) */ 2115 for (int i=0; i<g_srv_count; i++) { 2116 if (cli_nick && !cli_host) strncpy(g_srv[i].nick, cli_nick, MAX_NICK-1); 2117 if (cli_tls>=0 && !cli_host) g_srv[i].use_tls = cli_tls; 2118 if (cli_chan && !cli_host) srv_add_autojoin(i, cli_chan); 2119 } 2120 2121 /* create status channels */ 2122 for(int i=0;i<g_srv_count;i++) chan_add(i,"*status*"); 2123 2124 /* event pipe */ 2125 if(pipe(g_evpipe)<0){perror("pipe");return 1;} 2126 fcntl(g_evpipe[1],F_SETFL,O_NONBLOCK); 2127 fcntl(g_evpipe[0],F_SETFL,O_NONBLOCK); 2128 signal(SIGPIPE,SIG_IGN); 2129 2130 initscr(); raw(); noecho(); 2131 keypad(stdscr,TRUE); nodelay(stdscr,TRUE); curs_set(2); 2132 init_colors(); build_windows(); 2133 2134 /* start all server threads */ 2135 for(int i=0;i<g_srv_count;i++) start_net_thread(i); 2136 2137 while(1) { 2138 Event ev; 2139 while(read(g_evpipe[0],&ev,sizeof(Event))==(ssize_t)sizeof(Event)) 2140 handle_event(&ev); 2141 draw_all(); 2142 int key=getch(); 2143 if(key!=ERR) handle_key(key); 2144 struct timespec ts={0,20000000}; 2145 nanosleep(&ts,NULL); 2146 } 2147 endwin(); return 0; 2148 }