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