#include #include #include #include #include "irc.h" #include "screen.h" #include "edbox.h" #include "numerics.h" #include "keyclick.h" #include #include #include "nio.h" #include "config.h" #define MAX_MSG 512 char *msg_src, *msg_cmd, *msg_dest, *msg_text; char *msg_args[MAX_MSG_ARGS]; int msg_argcount; char irc_away = 0; char bell_type; static char msgbuf[MAX_MSG] = { 0 }; static char *msg; /* with source removed */ static int msgbuf_len = 0; static char regged = 0, hilite = 0; static char scr_prev = SCR_PRIV; char numbuf[10]; char last_pm_nick[33]; char last_chan[33]; /* without a screen */ /* static void join_channel(void) { txbuf_set_str2("JOIN ", channel); txbuf_send(); } */ static void send_nick(void) { txbuf_set_str2("NICK ", conf->nick); txbuf_send(); } static void print_reason(void) { if(msg_text) { scr_print_active(": "); scr_print_active(msg_text); } scr_eol_active(); } static void do_pong(void) { if(conf->show_ping) scr_print_server("PING/PONG\n"); txbuf_set_str2("PONG ", msg_args[0]); txbuf_send(); } static void bold(void) { scr_print_active("\x02"); } static void hilite_bold(void) { if(hilite) bold(); } static void do_chan_nick(void) { if(hilite) bell(); hilite_bold(); scr_print_active("<"); hilite_bold(); scr_print_active(msg_src); if(scr_active == SCR_SERVER) { /* if we don't have a window for it */ scr_print_active("/"); scr_print_active(msg_dest); } hilite_bold(); scr_print_active(">"); hilite_bold(); scr_print_active(" "); } static void do_priv_nick(void) { if(msg_src) { scr_print_active("*"); scr_print_active(msg_src); scr_print_active("* "); bell(); } } /* this ping calculation relies on the assumption that nobody's ping time will ever be more than 9 minutes. anyone that lagged will have been disconnected from the server already. if this assumption turns out to be false, the ping time displayed will be wrong (module ~9 mins). I don't think it's ever going to be a real problem. Why do it this way? Because using longs instead of ints bloats the code by 778 bytes! */ static void print_ping_time(char *p) { static unsigned int now, pingtime; static unsigned int sec, frac; now = read_rtclok(); pingtime = (unsigned int)atoi(p); /* correct for rtclock rollover (every ~9 mins) */ if(now < pingtime) now |= 0x8000; pingtime = now - pingtime; sec = pingtime / hz; frac = pingtime % hz; frac *= 1000; frac /= (hz * 10); scr_print_active("*** "); scr_print_active(msg_src); scr_print_active(" ping time: "); itoa(sec, numbuf, 10); scr_print_active(numbuf); scr_print_active("."); itoa(frac, numbuf, 10); scr_print_active(numbuf); scr_print_active(" sec"); /* // for debugging: scr_print_active(" "); itoa(pingtime, numbuf, 10); scr_print_active(numbuf); */ } static void do_server_pong(void) { msg_src = "Server"; // FIXME: what if the server doesn't :-quote the response? print_ping_time(msg_text); } /* FIXME: this isn't very fast */ static void do_ctcp(int is_notice) { static char *p, *ctcp_type, *resp; resp = 0; ctcp_type = ++msg_text; /* skip leading ^A */ if( (p = strchr(msg_text, '\x01')) ) { /* kill trailing ^A */ *p = 0; } if( (p = strchr(msg_text, ' ')) ) { *p++ = 0; } if(is_notice) { /* NOTICEs are responses */ if(p && streq_i(ctcp_type, "PING")) { print_ping_time(p); } else { scr_print_active("*** CTCP "); scr_print_active(ctcp_type); scr_print_active(" response from "); scr_print_active(msg_src); if(p) { scr_print_active(": "); scr_print_active(p); } } scr_eol_active(); } else { /* this is a PRIVMSG (aka a request) */ if(streq_i(ctcp_type, "ACTION")) { scr_print_active("* "); scr_print_active(msg_src); scr_print_active(" "); scr_print_active(p); scr_eol_active(); return; } scr_print_active("*** CTCP "); scr_print_active(ctcp_type); scr_print_active(" request from "); scr_print_active(msg_src); scr_eol_active(); if(streq_i(ctcp_type, "PING")) { resp = p; } else if(streq_i(ctcp_type, "CLIENTINFO")) { resp = "PING VERSION CLIENTINFO"; } else if(streq_i(ctcp_type, "VERSION")) { resp = "FujiNetChat pre-alpha on an Atari 8-bit"; } else { /* unknown CTCP type, ignore */ return; } txbuf_set_str3("NOTICE ", msg_src, " :\x01"); txbuf_append_str3(ctcp_type, " ", resp); txbuf_append_str("\x01"); txbuf_send(); } } static void do_privmsg(void) { /* TODO: this shouldn't be case-sensitive */ if(strstr(msg_text, conf->nick)) hilite = 1; else hilite = 0; if(*msg_text == '\x01') { do_ctcp(0); return; } if(*msg_dest == '#') do_chan_nick(); else do_priv_nick(); scr_print_active(msg_text); scr_eol_active(); } static void do_notice(void) { if(*msg_text == '\x01') { do_ctcp(1); } else { scr_print_active("NOTICE "); do_privmsg(); } } static void do_join(void) { if(streq_i(conf->nick, msg_src)) { scr_print_active("You have "); } else { scr_print_active(msg_src); scr_print_active(" has "); } scr_print_active("joined "); scr_print_active(msg_dest); scr_eol_active(); } static void do_nick(void) { if(streq_i(conf->nick, msg_src)) { scr_print_active("You are "); strncpy(conf->nick, msg_dest, 32); } else { scr_print_active(msg_src); scr_print_active(" is "); } scr_print_active("now known as "); scr_print_active(msg_dest); scr_eol_active(); } static void do_quit(void) { scr_print_active(msg_src); scr_print_active(" has quit IRC"); print_reason(); } static void do_part(void) { scr_print_active(msg_src); scr_print_active(" has left "); scr_print_active(msg_dest); if(msg_text) { scr_print_active(": "); scr_print_active(msg_text); } scr_eol_active(); } static void do_topic(void) { scr_print_active(msg_src); scr_print_active(" has set the topic of "); scr_print_active(msg_dest); scr_print_active(" to: "); scr_print_active(msg_text); scr_eol_active(); /* TODO: set topic in the screen! */ } static void do_kick(void) { scr_print_active(msg_src); scr_print_active(" has kicked "); scr_print_active(msg_args[1]); scr_print_active(" from "); scr_print_active(msg_dest); print_reason(); } static void do_mode(void) { int i; if(msg_src) scr_print_active(msg_src); else scr_print_active("Server"); scr_print_active(" sets mode: "); for(i = 0; i < msg_argcount; i++) { scr_print_active(msg_args[i]); scr_print_active(" "); } if(msg_text) scr_print_active(msg_text); scr_eol_active(); } /* numerics call this with arg==1, since arg 0 is always our nick. other commands call with arg==0 to print everything. */ static void do_catchall(int arg) { if(msg_src) { scr_print_active(msg_src); scr_print_active(" "); } scr_print_active(msg_cmd); for(; arg < msg_argcount; arg++) { scr_print_active(" "); scr_print_active(msg_args[arg]); } if(msg_text) { scr_print_active(" "); scr_print_active(msg_text); } scr_eol_active(); } /* permutes last character (doesn't add one), so for "Bob" you get: Bo_, Bo1 through Bo9, BoA through BoZ Gives a total of 36 replacement nicks to try. Eventually we run out and start repeating, but by then the IRC server will have disconnected us. */ static void permute_nick(void) { char *last; last = conf->nick + strlen(conf->nick) - 1; if((*last >= '1' && *last < '9') || (*last >= 'A' && *last < 'Z')) { (*last)++; } else { switch(*last) { case '_': *last = '1'; break; case '9': *last = 'A'; break; default: *last = '_'; break; } } } static void do_numeric(void) { unsigned int num = atoi(msg_cmd); switch(num) { /* use the server's idea of what our nick is, in case it got truncated. */ case RPL_WELCOME: strcpy(conf->nick, msg_args[0]); regged = 1; do_catchall(1); break; /* case RPL_WHOISCERTFP: case RPL_WHOISREGNICK: case RPL_WHOISUSER: case RPL_WHOISSERVER: case RPL_WHOISOPERATOR: case RPL_WHOISIDLE: case RPL_WHOISCHANNELS: case RPL_WHOISSPECIAL: case RPL_WHOISACCOUNT: case RPL_WHOISACTUALLY: case RPL_WHOISHOST: case RPL_WHOISMODES: case RPL_WHOISSECURE: case RPL_WHOWASUSER: These always go to the server window. */ case ERR_NICKNAMEINUSE: do_catchall(0); if(!regged) { permute_nick(); send_nick(); } break; /* don't print these, just noise */ case RPL_MOTDSTART: break; case RPL_MOTD: /* FIXME: this prevents the user using /MOTD on purpose, too */ if(!conf->hide_motd) do_catchall(0); break; /* don't print, but do trigger rejoin */ case RPL_ENDOFMOTD: case ERR_NOMOTD: cmd_rejoin_chans(); break; case RPL_NAMREPLY: scr_print_active(msg_args[2]); scr_print_active(" users: "); scr_print_active(msg_text); scr_eol_active(); break; case RPL_ENDOFNAMES: /* don't print, just noise */ break; case RPL_TOPIC: scr_print_active("Topic for "); scr_print_active(msg_args[1]); scr_print_active(": "); scr_print_active(msg_text); scr_eol_active(); break; case RPL_TOPICWHOTIME: scr_print_active("Topic set by "); scr_print_active(msg_args[2]); scr_eol_active(); break; default: do_catchall(1); break; } } static void invalid_msg(char type) { scr_print(SCR_SERVER, "??? unknown, type "); scr_putc(SCR_SERVER, type); scr_putc(SCR_SERVER, '\n'); } void select_screen(void) { char s; if(!msg_dest) { s = SCR_SERVER; } else if(*msg_dest == '#') { s = scr_getbyname(msg_dest); if(!s) { strncpy(last_chan, msg_dest, 32); s = SCR_SERVER; } } else { s = scr_getbyname(msg_src); if(!s) { if(streq_i(msg_cmd, "PRIVMSG")) { /* or maybe NOTICE? */ strncpy(last_pm_nick, msg_src, 32); s = SCR_PRIV; } else { s = SCR_SERVER; } } } scr_activate(s); } static void dispatch_msg(void) { /* at this point, we know the message source and destination, so: */ select_screen(); if(streq_i(msg_cmd, "PRIVMSG")) { do_privmsg(); } else if(streq_i(msg_cmd, "NOTICE")) { do_notice(); } else if(streq_i(msg_cmd, "JOIN")) { do_join(); } else if(streq_i(msg_cmd, "NICK")) { do_nick(); } else if(streq_i(msg_cmd, "QUIT")) { do_quit(); } else if(streq_i(msg_cmd, "PART")) { do_part(); } else if(streq_i(msg_cmd, "TOPIC")) { do_topic(); } else if(streq_i(msg_cmd, "KICK")) { do_kick(); } else if(streq_i(msg_cmd, "MODE")) { do_mode(); } else if(streq_i(msg_cmd, "PONG")) { do_server_pong(); } else if(isdigit(msg_cmd[0])) { do_numeric(); } else { do_catchall(0); } } /* msgbuf contains a complete message from the server, whose length is msgbuf_len. The EOL that was at the end of the message is *not* present here (been replaced with a null already). */ static void parse_msg(void) { char *p; msg_cmd = msg_text = msg_src = msg_dest = 0; msg = msgbuf; /* ignore empty message */ if(!*msg) return; /* scr_print_active("RAW: "); scr_print_active(msg); scr_eol_active(); */ /* if there's a final multiword arg... */ /* FIXME: channel names can have colons, which breaks this... */ p = strstr(msg + 1, " :"); /* +1 to skip leading colon in msg source */ if(p) { msg_text = p + 2; *p = 0; } /* first token is either the source (with a :) or a command (without) */ p = strtok(msg, " "); if(!p) { invalid_msg('1'); return; } if(*p == ':') { msg_src = p + 1; /* generally :irc.example.com or :nick!user@host */ msg_cmd = strtok(0, " "); } else { msg_src = 0; /* no source supplied */ msg_cmd = p; } if(!msg_cmd) { invalid_msg('2'); return; } /* special case for ping, treat as 1 arg, even if it has space and no : */ if(streq_i(msg_cmd, "PING")) { msg_argcount = 1; msg_args[0] = msg_cmd + 6; do_pong(); return; } else { for(msg_argcount = 0; msg_argcount < MAX_MSG_ARGS; msg_argcount++) { p = strtok(0, " "); if(p) { msg_args[msg_argcount] = p; /* if any arg is a channel name, use it for the dest */ if(*p == '#') msg_dest = p; } else { break; } } } /* if(msg_dest) { scr_print_current("got here, msg_dest is: "); scr_print_current(msg_dest); scr_eol_current(); } */ if(!msg_dest) { if(msg_argcount) msg_dest = msg_args[0]; else if(msg_text) msg_dest = msg_text; } if(msg_src) { if((p = strstr(msg_src, "!"))) { *p = '\0'; } else if(strstr(msg_src, ".")) { msg_src = 0; } } dispatch_msg(); } /* we read via block I/O, so split into lines and process each one separately. */ static void irc_split_Lines(void) { int i; char *p = rx_buf; for(i = 0; i < rxbuflen; i++) { msgbuf[msgbuf_len] = *p; if(*p == CH_EOL) { // msgbuf[msgbuf_len + 1] = '\0'; /* do not include the EOL */ msgbuf[msgbuf_len] = '\0'; parse_msg(); msgbuf_len = 0; } else { msgbuf_len++; } p++; } } /* TODO: there needs to be a scr_printnum() */ void print_errnum(void) { extern unsigned char err; scr_print_current("Error #"); itoa(err, numbuf, 10); scr_print_current(numbuf); } int irc_read(void) { if(!trip) return 1; err = nstatus(conf->url); if(err != 1) { scr_display(SCR_SERVER); regged = 0; if(err == 136) { scr_print_current("Disconnected"); } else { print_errnum(); } scr_eol_current(); bell(); return 0; } // Get # of bytes waiting, no more than size of rx_buf rxbuflen = (OS.dvstat[1] << 8) | OS.dvstat[0]; if(rxbuflen > MAX_MSG) rxbuflen = MAX_MSG; if(rxbuflen > 0) { err = nread(conf->url, rx_buf, rxbuflen); if(err != 1) { print_errnum(); return 0; } trip = 0; PIA.pactl |= 1; // Flag interrupt as serviced, ready for next one. irc_split_Lines(); } return 1; } /* modern.ircdocs.horse say to do this IMMEDIATELY upon TCP connection, without waiting for anything from the server. */ void irc_register(void) { if(regged) return; /* 2nd arg: local (UNIX) username, just use the nick */ /* 3rd arg: "real" name */ txbuf_set_str3("USER ", conf->nick, " 0 * :"); txbuf_append_str(conf->real_name); txbuf_send(); send_nick(); } static void scrollback() { OS.ch = 0xff; scr_scrollback(); while(OS.ch == 0xff) /* NOP */ ; keyclick(); OS.ch = 0xff; scr_end_scrollback(); } static void hunt_screen(signed char dir) { signed char s = scr_current; do { s += dir; if(s < 0) s = MAX_SCREENS - 1; s %= MAX_SCREENS; } while(scr_status[s] == SCR_UNUSED); scr_display(s); } static char *get_cur_chan(void) { if(scr_current == SCR_SERVER && last_chan[0]) return last_chan; else if ((scr_current > 1) && (scr_names[scr_current][0] == '#')) return scr_names[scr_current]; else return 0; } static char *get_cur_nick(void) { if(scr_current == SCR_PRIV && last_pm_nick[0]) return last_pm_nick; else if (scr_current > 1 && scr_names[scr_current][0] != '#') return scr_names[scr_current]; else return 0; } static void send2_with_space(char *s1, char *s2) { txbuf_set_str3(s1, " ", s2); txbuf_send(); } static void send_cur_nick_cmd(char *cmd) { char *nick; if(!(nick = get_cur_nick())) return; send2_with_space(cmd, nick); } static void send_cur_chan_cmd(char *cmd) { char *chan; if(!(chan = get_cur_chan())) return; send2_with_space(cmd, chan); } /* static char cur_is_chan(void) { return (scr_current > 2 && scr_names[scr_current][0] == '#'); } static char cur_is_query(void) { return (scr_current > 2 && !cur_is_chan()); } */ void switch_to_active() { char i; for(i = 0; i < MAX_SCREENS; i++) { if(scr_status[i] == SCR_ACTIVE) { scr_prev = scr_current; scr_display(i); return; } } } void list_screens(void) { char i; scr_activate(scr_current); for(i = 0; i < MAX_SCREENS; i++) { bold(); scr_putc_active(i + '1'); bold(); scr_putc_active(':'); if(scr_status[i] == SCR_UNUSED) scr_print_active(""); else scr_print_active(scr_names[i]); scr_eol_active(); } } void join_last_invite(void) { // TODO! } void ui_ping(void) { char *nick; if(!(nick = get_cur_nick())) return; cmd_ctcp_ping(nick); } static void start_keystroke(void) { char i, s; i = cgetc(); if(i >= '1' && i <= '7') { s = i - '1'; if(s != scr_current) { if(scr_status[s] != SCR_UNUSED) { scr_prev = scr_current; scr_display(s); } } return; } switch(tolower(i)) { case CH_CURS_UP: case '-': scrollback(); return; case 0x18: /* ^X */ send_cur_chan_cmd("PART"); /* fall thru */ case CH_ESC: scr_prev = SCR_PRIV; scr_destroy(scr_current); return; case CH_CURS_LEFT: case '+': scr_prev = scr_current; hunt_screen(-1); return; case CH_CURS_RIGHT: case '*': scr_prev = scr_current; hunt_screen(1); return; case CH_TAB: i = scr_current; scr_display(scr_prev); scr_prev = i; return; case 'q': if(scr_current == SCR_PRIV && *last_pm_nick) { scr_prev = scr_current; scr_create(last_pm_nick, 1); *last_pm_nick = 0; } else if(scr_current == SCR_SERVER && *last_chan) { scr_prev = scr_current; scr_create(last_chan, 1); *last_chan = 0; } return; case 'a': switch_to_active(); return; case 'l': list_screens(); return; case 'j': join_last_invite(); return; case 'w': send_cur_nick_cmd("WHOIS"); return; case 'n': send_cur_chan_cmd("NAMES"); return; case 'p': ui_ping(); return; case 't': send_cur_chan_cmd("TOPIC"); break; case 's': edbox_hide(); /* fall thru */ default: break; } } static void keystroke(void) { if(OS.ch == 0xff) return; if(irc_away) { txbuf_send_str("AWAY"); irc_away = 0; } if(GTIA_READ.consol == 6) { /* start pressed */ start_keystroke(); } else { edbox_keystroke(); } } /* only exits on error (e.g. connection closed, which might be via /QUIT). */ void irc_loop(void) { while(1) { if(conf->atract_away) { if(!irc_away && (OS.atract & 0x80)) { irc_away = 1; txbuf_send_str("AWAY :ATRACT mode"); } } if(!irc_read()) return; keystroke(); } }