aboutsummaryrefslogtreecommitdiffstats
path: root/c/irc.c
blob: 250738aca00c14df0a9078e5e29d94e9ad22fa88 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
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);
}