aboutsummaryrefslogtreecommitdiffstats
path: root/usr/irc.c
diff options
context:
space:
mode:
Diffstat (limited to 'usr/irc.c')
-rw-r--r--usr/irc.c422
1 files changed, 422 insertions, 0 deletions
diff --git a/usr/irc.c b/usr/irc.c
new file mode 100644
index 0000000..250738a
--- /dev/null
+++ b/usr/irc.c
@@ -0,0 +1,422 @@
+#include "resolve.h"
+/* irc HOST [NICK] - a bitchx/irssi-flavored IRC client on 40x18 columns.
+ The kernel owns TCP; this owns the protocol and the screen.
+
+ UI: messages scroll above; the bottom line is an irssi-style input line
+ "[#chan] text_" redrawn in place with \r (the terminal has no cursor
+ addressing - \r + overprint is the entire trick). Long input scrolls
+ horizontally like irssi. Keys come from the OSK (SELECT toggles it) and
+ from the console ring (pollcon), so a hub can type into the client too.
+
+ Formats, straight from the elders:
+ <nick> text channel message * nick text CTCP ACTION
+ <nick:#c> text msg to a non-current channel
+ *nick* text private msg >target< text outbound /msg
+ -nick- text notice -!- text server/status */
+
+/* seg first + large: the arg string lives at 0xA000 and the first static
+ lands on it; args are copied out in main() before seg is ever written. */
+static unsigned char seg[220]; /* one TCP segment from the kernel */
+static char line[240]; /* server line accumulator */
+static unsigned char llen;
+static char input[140]; /* the input line being typed */
+static unsigned char inlen;
+static unsigned char drawn; /* chars currently drawn on input row */
+static char nick[16];
+static char target[32]; /* current channel/query, "" = none */
+static char sbuf[200]; /* outgoing IRC line builder */
+static unsigned char slen;
+static unsigned char namebuf[64];
+static unsigned char dst[4];
+static unsigned char sock;
+static unsigned char quitting;
+/* A WRITABLE empty string for parse defaults. handle() writes terminators
+ into pfx (bang) and txt (strip_fmt); a read-only "" literal lives in ROM
+ ($4000-$5FFF), where a write is an MBC5 *RAM-bank-select* - it would swap
+ the whole $A000 data bank out from under every static (instant garbage).
+ A prefix-less or empty-trailing server line is enough to trigger it. */
+static char empty[1];
+
+/* ---- tiny string helpers (SDCC libc is us) ------------------------------- */
+static unsigned char sceq(const char *a, const char *b) { /* case-insens. */
+ unsigned char ca, cb;
+ for (;;) {
+ ca = (unsigned char)*a++; cb = (unsigned char)*b++;
+ if (ca >= 'a' && ca <= 'z') ca -= 32;
+ if (cb >= 'a' && cb <= 'z') cb -= 32;
+ if (ca != cb) return 0;
+ if (!ca) return 1;
+ }
+}
+static void scpy(char *d, const char *s, unsigned char max) {
+ unsigned char i;
+ for (i = 0; s[i] && i < (unsigned char)(max - 1); i++) d[i] = s[i];
+ d[i] = 0;
+}
+
+/* ---- outgoing IRC lines --------------------------------------------------- */
+static void sc(const char *s) { /* append to sbuf */
+ while (*s && slen < 197) sbuf[slen++] = *s++;
+}
+static void sfin(void) { /* CRLF + send */
+ sbuf[slen++] = '\r'; sbuf[slen++] = '\n';
+ net_send(sock, sbuf, slen);
+ slen = 0;
+}
+
+/* ---- the input line ------------------------------------------------------- */
+/* erase what's drawn on the bottom row, park at col 0 */
+static void row_clear(void) {
+ unsigned char i;
+ putc('\r');
+ for (i = 0; i < drawn; i++) putc(' ');
+ putc('\r');
+ drawn = 0;
+}
+/* redraw "[target] tail-of-input_"; keep total < 40 so it never wraps */
+static void row_draw(void) {
+ unsigned char pl, vis, i;
+ const char *t = target[0] ? target : "(status)";
+ row_clear();
+ putc('[');
+ for (pl = 0; t[pl] && pl < 12; pl++) putc(t[pl]);
+ putc(']'); putc(' ');
+ pl += 3; /* prompt width */
+ vis = 39 - pl - 1; /* room for text (cursor gets 1) */
+ i = (inlen > vis) ? (unsigned char)(inlen - vis) : 0;
+ for (; i < inlen; i++) putc(input[i]);
+ drawn = pl + ((inlen > vis) ? vis : inlen);
+}
+/* print a message line above the input line */
+static void say(const char *s) {
+ row_clear();
+ puts(s); nl();
+ row_draw();
+}
+static void say2(const char *a, const char *b) {
+ row_clear();
+ puts(a); puts(b); nl();
+ row_draw();
+}
+static void say3(const char *a, const char *b, const char *c) {
+ row_clear();
+ puts(a); puts(b); puts(c); nl();
+ row_draw();
+}
+
+/* ---- server line parsing -------------------------------------------------- */
+/* split "pfx!user@host" -> pfx nick only (in place) */
+static void bang(char *p) {
+ while (*p && *p != '!') p++;
+ *p = 0;
+}
+static unsigned char isnum(const char *c) {
+ return c[0] >= '0' && c[0] <= '9';
+}
+static unsigned char ishex(char c) {
+ return (c>='0'&&c<='9')||(c>='a'&&c<='f')||(c>='A'&&c<='F');
+}
+
+/* Strip mIRC/IRC formatting in place - crucially their *arguments*, or a
+ color logo (a real MOTD!) leaves its \x03 color numbers on screen as digit
+ soup. \x03 = color (up to 2 digits[,up to 2 digits]); \x04 = hex color
+ (up to 6 hex[,6 hex]); \x02 bold \x0f reset \x11 mono \x16 reverse
+ \x1d italic \x1e strike \x1f underline are lone toggles. Leaves \x01
+ (CTCP) intact for the PRIVMSG path. */
+static void strip_fmt(char *s) {
+ char *w = s, *r = s;
+ unsigned char c, d;
+ while (*r) {
+ c = (unsigned char)*r;
+ if (c == 3) {
+ r++; d = 0;
+ while (*r>='0'&&*r<='9'&&d<2) { r++; d++; }
+ if (*r==','&&r[1]>='0'&&r[1]<='9') { r++; d=0; while(*r>='0'&&*r<='9'&&d<2){r++;d++;} }
+ continue;
+ }
+ if (c == 4) {
+ r++; d = 0;
+ while (ishex(*r)&&d<6) { r++; d++; }
+ if (*r==','&&ishex(r[1])) { r++; d=0; while(ishex(*r)&&d<6){r++;d++;} }
+ continue;
+ }
+ if (c==2||c==15||c==17||c==22||c==29||c==30||c==31) { r++; continue; }
+ *w++ = *r++;
+ }
+ *w = 0;
+}
+
+static void handle(char *l) {
+ char *pfx = empty, *cmd, *arg, *txt = empty;
+ char *p = l;
+
+ if (*p == ':') { /* :prefix */
+ pfx = ++p;
+ while (*p && *p != ' ') p++;
+ if (*p) *p++ = 0;
+ }
+ cmd = p; /* command */
+ while (*p && *p != ' ') p++;
+ if (*p) *p++ = 0;
+ arg = p; /* params, then :trailing */
+ while (*p) {
+ if (*p == ':' && (p == arg || p[-1] == 0)) { txt = p + 1; p[-1] = 0; break; }
+ if (*p == ' ') *p = 0;
+ p++;
+ }
+ /* arg = first param (NUL-joined list), txt = trailing */
+ strip_fmt(txt); /* kill color/format codes + args */
+
+ if (sceq(cmd, "PING")) {
+ slen = 0; sc("PONG :"); sc(txt[0] ? txt : arg); sfin();
+ say("-!- Ping? Pong!");
+ return;
+ }
+ bang(pfx);
+ if (sceq(cmd, "PRIVMSG")) {
+ char *dest = arg;
+ unsigned char act = 0;
+ if (txt[0] == 1) { /* CTCP */
+ char *e = txt + 1;
+ while (*e && *e != 1) e++;
+ *e = 0; txt++;
+ if (txt[0]=='A'&&txt[1]=='C'&&txt[2]=='T'&&txt[3]=='I'&&txt[4]=='O'&&txt[5]=='N'&&txt[6]==' ') {
+ act = 1; txt += 7;
+ } else if (txt[0]=='V') { /* VERSION */
+ slen = 0; sc("NOTICE "); sc(pfx); sc(" :\001VERSION gbos:sl0pboy:GameBoyColor\001"); sfin();
+ return;
+ } else return;
+ }
+ row_clear();
+ if (act) { puts("* "); puts(pfx); putc(' '); }
+ else if (sceq(dest, nick)) { putc('*'); puts(pfx); puts("* "); }
+ else if (sceq(dest, target)) { putc('<'); puts(pfx); puts("> "); }
+ else { putc('<'); puts(pfx); putc(':'); puts(dest); puts("> "); }
+ puts(txt); nl();
+ row_draw();
+ return;
+ }
+ if (sceq(cmd, "NOTICE")) {
+ row_clear();
+ putc('-'); puts(pfx[0] ? pfx : "server"); puts("- "); puts(txt); nl();
+ row_draw();
+ return;
+ }
+ if (sceq(cmd, "JOIN")) {
+ char *ch = txt[0] ? txt : arg;
+ if (sceq(pfx, nick)) {
+ scpy(target, ch, sizeof(target));
+ say2("-!- now talking in ", target);
+ } else say3("-!- ", pfx, " has joined");
+ return;
+ }
+ if (sceq(cmd, "PART")) {
+ if (sceq(pfx, nick)) { target[0] = 0; say("-!- you left"); }
+ else say3("-!- ", pfx, " has left");
+ return;
+ }
+ if (sceq(cmd, "QUIT")) { say3("-!- ", pfx, " has quit"); return; }
+ if (sceq(cmd, "KICK")) {
+ char *ch = arg, *who = arg;
+ while (*who) who++;
+ who++; /* param 2: the victim */
+ if (sceq(who, nick)) { target[0] = 0; say2("-!- you were kicked from ", ch); }
+ else say3("-!- ", who, " was kicked");
+ return;
+ }
+ if (sceq(cmd, "NICK")) {
+ char *nn = txt[0] ? txt : arg;
+ if (sceq(pfx, nick)) scpy(nick, nn, sizeof(nick));
+ row_clear();
+ puts("-!- "); puts(pfx); puts(" is now known as "); puts(nn); nl();
+ row_draw();
+ return;
+ }
+ if (isnum(cmd)) {
+ if (cmd[0]=='4'&&cmd[1]=='3'&&cmd[2]=='3') { /* nick in use */
+ unsigned char n = strlen(nick);
+ if (n < 14) { nick[n] = '_'; nick[n+1] = 0; }
+ slen = 0; sc("NICK "); sc(nick); sfin();
+ say2("-!- nick in use, trying ", nick);
+ return;
+ }
+ if (cmd[0]=='3'&&cmd[1]=='3'&&cmd[2]=='2') { /* topic */
+ say2("-!- topic: ", txt);
+ return;
+ }
+ if (cmd[0]=='3'&&cmd[1]=='5'&&cmd[2]=='3') { /* names */
+ say2("-!- users: ", txt);
+ return;
+ }
+ if (cmd[1]=='3'&&cmd[0]=='3') return; /* 333 topic-by: skip */
+ if (cmd[0]=='0'&&cmd[1]=='0'&&(cmd[2]=='4'||cmd[2]=='5')) return; /* MYINFO/ISUPPORT noise */
+ if (txt[0]) say2("-!- ", txt); /* motd & friends */
+ return;
+ }
+ /* anything else: show it raw-ish, bitchx would too */
+ if (txt[0]) say3(cmd, " ", txt);
+}
+
+/* accumulate TCP bytes into lines */
+static void feed(unsigned char *b, unsigned char n) {
+ unsigned char i;
+ for (i = 0; i < n; i++) {
+ char c = (char)b[i];
+ if (c == '\r') continue;
+ if (c == '\n') { line[llen] = 0; if (llen) handle(line); llen = 0; }
+ else if (llen < 239) line[llen++] = c;
+ }
+}
+
+/* ---- typed input ---------------------------------------------------------- */
+static void run_cmd(char *t) {
+ char *a = t;
+ while (*a && *a != ' ') a++;
+ if (*a) *a++ = 0; /* t = /cmd, a = rest */
+
+ if (sceq(t, "/join") || sceq(t, "/j")) {
+ if (!*a) { say("-!- join what?"); return; }
+ slen = 0; sc("JOIN "); sc(a); sfin();
+ return;
+ }
+ if (sceq(t, "/part") || sceq(t, "/p")) {
+ slen = 0; sc("PART "); sc(*a ? a : target); sfin();
+ return;
+ }
+ if (sceq(t, "/msg")) {
+ char *m = a;
+ while (*m && *m != ' ') m++;
+ if (*m) *m++ = 0;
+ if (!*a || !*m) { say("-!- /msg nick text"); return; }
+ slen = 0; sc("PRIVMSG "); sc(a); sc(" :"); sc(m); sfin();
+ row_clear();
+ putc('>'); puts(a); puts("< "); puts(m); nl();
+ row_draw();
+ return;
+ }
+ if (sceq(t, "/me")) {
+ if (!target[0]) { say("-!- no channel"); return; }
+ slen = 0; sc("PRIVMSG "); sc(target); sc(" :\001ACTION "); sc(a); sc("\001"); sfin();
+ row_clear();
+ puts("* "); puts(nick); putc(' '); puts(a); nl();
+ row_draw();
+ return;
+ }
+ if (sceq(t, "/nick")) {
+ if (!*a) { say("-!- /nick newnick"); return; }
+ slen = 0; sc("NICK "); sc(a); sfin();
+ return;
+ }
+ if (sceq(t, "/quit")) {
+ slen = 0; sc("QUIT :"); sc(*a ? a : "sl0pboy out"); sfin();
+ quitting = 1;
+ return;
+ }
+ if (sceq(t, "/raw") || sceq(t, "/quote")) {
+ slen = 0; sc(a); sfin();
+ return;
+ }
+ say("-!- commands: /join /part /msg /me /nick /quit /raw");
+}
+
+static void submit(void) {
+ input[inlen] = 0;
+ if (!inlen) return;
+ if (input[0] == '/') {
+ inlen = 0;
+ run_cmd(input);
+ row_draw();
+ return;
+ }
+ if (!target[0]) { inlen = 0; say("-!- join a channel first (/join #gb)"); return; }
+ slen = 0; sc("PRIVMSG "); sc(target); sc(" :"); sc(input); sfin();
+ row_clear();
+ putc('<'); puts(nick); puts("> "); puts(input); nl();
+ inlen = 0;
+ row_draw();
+}
+
+static void key(unsigned char c) {
+ if (c == '\n' || c == '\r') { submit(); return; }
+ if (c == 8 || c == 127) {
+ if (inlen) { inlen--; row_draw(); }
+ return;
+ }
+ if (c < 32) return;
+ if (inlen < 139) { input[inlen++] = (char)c; row_draw(); }
+}
+
+/* ---- main ----------------------------------------------------------------- */
+void main(void) {
+ char *argp;
+ unsigned char i, n, idle;
+
+ /* gbos does not zero C statics (crt0 zeroes nothing; fork inherits the
+ parent's RAM bank) - initialize every piece of state explicitly */
+ llen = inlen = drawn = slen = quitting = 0;
+ target[0] = 0;
+ empty[0] = 0; /* gbos doesn't zero BSS */
+
+ argp = getargs(); /* copy args before seg is used */
+ for (i = 0; argp[i] && i < 63; i++) namebuf[i] = (unsigned char)argp[i];
+ namebuf[i] = 0;
+
+ /* split "HOST [NICK]" */
+ scpy(nick, "sl0pboy", sizeof(nick));
+ for (i = 0; namebuf[i]; i++)
+ if (namebuf[i] == ' ') {
+ namebuf[i] = 0;
+ if (namebuf[i+1]) scpy(nick, (char *)namebuf + i + 1, sizeof(nick));
+ break;
+ }
+ if (!namebuf[0]) {
+ puts("usage: irc HOST [NICK] (port 6667)"); nl();
+ puts(" then: /join #chan, SELECT = keyboard"); nl();
+ sexit(1);
+ }
+
+ if (!resolve((char *)namebuf, dst)) {
+ puts("irc: cannot resolve "); puts((char *)namebuf); nl(); sexit(1);
+ }
+
+ sock = net_socket(SOCK_TCP);
+ if (sock == 0xFF) { puts("irc: no socket"); nl(); sexit(1); }
+ /* vary the local port per run (DIV timer noise): a fixed port can hit a
+ stale half-dead connection at the server after an unclean exit */
+ net_bind(sock, 41000 + ((unsigned int)*(volatile unsigned char *)0xFF04 << 2) + getpid());
+ puts("-!- connecting to "); puts((char *)namebuf); puts(":6667"); nl();
+ if (net_connect(sock, dst, 6667) == 0xFF) {
+ puts("irc: connect failed"); nl(); net_close(sock); sexit(1);
+ }
+
+ slen = 0; sc("NICK "); sc(nick); sfin();
+ slen = 0; sc("USER gbos 0 * :sl0pboy on gbos"); sfin();
+ puts("-!- registering as "); puts(nick);
+ puts(" (SELECT = keyboard)"); nl();
+ row_draw();
+
+ idle = 0;
+ for (;;) {
+ n = net_recv_nb(sock, seg, (unsigned char)sizeof(seg));
+ if (n == 0) { say("-!- disconnected"); break; }
+ if (n != 0xFE && n != 0xFF) feed(seg, n); /* render a segment if any */
+ if (quitting) { say("-!- quit"); break; }
+
+ /* Poll input EVERY pass, even mid-flood: otherwise a big MOTD (or a
+ * busy channel) starves the keyboard for its whole duration - the OSK
+ * won't even open, and keystrokes are dropped. That's what made
+ * '/join #sl0p right after connect' look broken. */
+ i = pollin(); /* OSK */
+ if (!i) i = pollcon(); /* hub-injected console bytes */
+ if (i) key(i);
+
+ if (n == 0xFE || n == 0xFF) { /* nothing received this pass */
+ if (!i && ++idle >= 4) { msleep(20); idle = 4; }
+ } else {
+ idle = 0; /* actively receiving; don't sleep */
+ }
+ }
+ net_close(sock);
+ row_clear();
+ sexit(0);
+}