diff options
Diffstat (limited to 'src/main.c')
-rw-r--r-- | src/main.c | 1184 |
1 files changed, 1184 insertions, 0 deletions
diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..2e5ee17 --- /dev/null +++ b/src/main.c @@ -0,0 +1,1184 @@ +/* + * Copyright (c) 2008, Brian Watson + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgements: + * This product includes software developed by Adam Dunkels. + * This product includes software developed by Brian Watson. + * 4. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * This file is part of the FujiChat IRC client. + * + */ + + +#include <stdio.h> +#include <stdlib.h> +#include <atari.h> +#include "uip.h" +#include "uiplib.h" +#include "rs232dev.h" +#include <conio.h> +#include <peekpoke.h> +#include <string.h> +#include <ctype.h> +#include <rs232.h> + +#include "timer.h" + +#ifndef NULL +#define NULL (void *)0 +#endif /* NULL */ + +/* Program name (plain ASCII for network use) */ +#define SELF "FujiChat" +/* inverse video version, used for local prompts: */ +#define SELF_INV "\xc6\xf5\xea\xe9\xc3\xe8\xe1\xf4" + +#define VERSION "0.3.1" +#define BANNER SELF_INV " v" VERSION +#define VERSION_REPLY SELF " v" VERSION + + +#define DEFAULT_NICK SELF + +#define CONF_SIGNATURE "\x03\x0e" +#define CONF_VERSION 4 +#define DEFAULT_CONF_FILE "FUJICHAT.CFG" + +/* TODO: move the non-IRC-related config to a separate SETUP program, + and a separate UIP.CFG file. This will be shared by all apps that + use uIP, such as the planned telnet and FTP clients. Also it means + we don't have to bloat each program with a complete config UI + for things like the local/peer/DNS IP addresses. + + In fact the SETUP utility might resolve IP addresses for us, so + each app might not need its own copy of the resolver... though + that's maybe a bit clumsy to use for FTP, since you generally want + to easily be able to connect to arbitrary servers (unlike, say, IRC + where you usually use the same server all the time). Perhaps SETUP + could resolve arbitrary hostnames, then write them to a /etc/hosts + type of file. Parsing /etc/hosts entries is fairly lightweight, + could be done in all the client programs... + + ...or maybe what should happen is that there should be a RESOLV + program that can resolve hostnames, and read/write them in text + form to a HOSTS.TXT file... and also parses the HOSTS file and + stores the hostname strings and 4-byte IP addresses in a fixed + area of memory. The app programs could just look up hostnames in + this memory area, without having to open & parse HOSTS themselves. + cc65 programs by default start at $2E00. The Bob-verter driver ends + around $2200 (other R: drivers should be comparable), which gives + us around 3K of unallocated memory. Assuming the average hostname/IP + pair is 50 bytes, that'd give us about 60 of them. The RESOLV program + would stop the user from adding more hostnames when there's no space + for them (and let them delete some to make room, maybe commenting them + out so they remain in the HOSTS file for future uncommenting). + + Ultimately there should be a master AUTORUN.SYS file with a menu + that runs the other sub-apps (or lets you exit to DUP). Menu would + have options like "DNS", "IRC", "FTP", "Telnet", and "Network + Setup". Of course I need to write a program loader, there's no + exec* functions in cc65! The Network Setup option would let you + specify the R: driver you want to load, so it wouldn't have to + be prepended to any of the executables, and would be auto-loaded + the first time you try to run any of the apps that need it. It'd + be nice if the main menu program had at least a few DOS functions + as well (delete/copy/rename/lock/unlock, format too?) + + Probably, FujiChat's 1.0 release will be as a standalone IRC client, + with the current config UI stuff as-is. The other stuff would be + released months from now, under the label "FujiNet" or something, and + include a new version of FujiChat as one of several apps. + + The final distibution disk will need at least some documentation in + Atari-readable text format, plus a doc-reader program. + + All the subprograms need to end with RTS to the menu program, which + shold be small and stay resident in addresses not touched by cc65 + (I've got 128 bytes each in the cassette buffer and page 5, also + 256 more in page 6. Should be easy to squeeze into 512 bytes, no?) + + So a disk directory would contain: + + DOS.SYS, DUP.SYS + AUTORUN.SYS - the main menu and subprogram loader + BOBVERT.XEX - the Bob-verter R: driver + A850.XEX - the 850 R: driver (from DOS 2.0S I guess) + MIO.XEX, BLACKBOX.XEX, etc, drivers for other serial ports + HOSTS.TXT - the /etc/hosts file, human-readable and -editable + README.TXT - docs + MORE.XEX - doc reader (should handle ASCII or ATASCII EOLs) + SETUP.XEX - set up global network settings (local/remote/dns IPs, also + things like screen colors, key-repeat rate, etc). + DNS.XEX - lookup hostnames, maintain and parse HOSTS.TXT + FTP.XEX, TELNET.XEX, IRC.XEX, PING.XEX - the apps + FTP.CFG, TELNET.CFG, IRC.CFG, etc - app-specific settings + FUJINET.CFG - global settings + ...if there's room on the disk, a small text editor might be nice + to include. + + The whole thing should be distributed as a bootable, single-density + DOS disk (undecided which DOS, either 2.0S or MyDOS 4.5). + + Alternatively, all the XEX files might be named COM, and the whole + thing could run under a CLI DOS like Sparta (if it'll actually + run). The apps could take optional CLI arguments, also... and the + AUTORUN.SYS would actually be unnecessary! + + */ + +#define BUF_SIZE 256 +#define OUTBUF_SIZE 768 +#define NICKLEN 20 +#define HOSTLEN 64 + +/* fuji_conf_t is stored in the config file verbatim. + Don't forget to update CONF_VERSION when changing this struct! + Also, the first member should never, ever change, and the 2nd + (version) should only ever be set to CONF_VERSION (which can be + changed) + + TODO: + char alt_nick[NICKLEN+1]; + */ +typedef struct { + char signature[2]; + u16_t version; + + /* uIP settings: */ + uip_ipaddr_t local_ip; + uip_ipaddr_t peer_ip; + uip_ipaddr_t resolver_ip; + char baud; + + /* IRC settings: */ + char server[HOSTLEN+1]; + u16_t server_port; + char nick[NICKLEN+1]; + char real_name[NICKLEN+1]; + char channel[NICKLEN+1]; + + /* UI settings: */ + char bg_color; + char fg_color; + u16_t ui_flags; /* bitwise OR of UIFLAG_* constants */ +} fuji_conf_t; + +fuji_conf_t config; + +/* Whether or not to ring the bell on receiving private message */ +#define UIFLAG_MSGBELL 0x8000 + +/* Whether or not to show [PING,PONG] */ +#define UIFLAG_SHOWPING 0x4000 + +/* Visual bell (screen flash) instead of audible */ +#define UIFLAG_VBELL 0x2000 + +/* Hide (don't display) the server MOTD */ +/* TODO: implement */ +#define UIFLAG_HIDEMOTD 0x1000 + +/* Disable bells entirely */ +#define UIFLAG_NOBELL 0x0800 + +char os_version[10] = "XL_XE"; +char channel[BUF_SIZE]; +char input_buf[BUF_SIZE]; +char chan_msg_buf[BUF_SIZE * 2]; +char output_buf[OUTBUF_SIZE + 1]; +char last_msg_nick[NICKLEN+1] = ""; + +int input_buf_len = 0; +int output_buf_len = 0; + +char config_valid = 0; +char done = 0; +char connected = 0; +char joined_channel = 0; /* true if we're in a channel */ +char nick_registered = 0; +char vbell_active = 0; + +static void get_line(char *buf, unsigned char len); +static void handle_keystroke(void); +static void handle_command(void); +static void get_config(void); +static void config_menu(void); +static void send_server_cmd(char *cmd, char *arg); +static void bell(); +void redraw_user_buffer(); + +/* ATASCII characters */ +#define A_EOL 0x9b +#define A_TAB 0x7f +#define A_BS 0x7e +#define A_CLR 0x7d +#define A_BEL 0xfd +#define A_DEL 0x9c + +/* plain ASCII characters */ +#define NL 0x0a +#define CR 0x0d +#define TAB 0x09 + +/* TODO: move these to an external file or otherwise come up with a way + to reuse the memory */ +#define SERVER_LIST_LEN 8 +static char *servers[][2] = { + /* + { "82.165.139.95", "irc.tobug.net (US)" }, + { "63.243.153.235", "irc.carterroadband.com (US)" }, + { "209.133.9.109", "ircd.eyearesee.org (US)" }, + { "89.238.135.210", "london.eyearesee.org (EU)" }, + { "193.23.141.104", "hungary.eyearesee.org (EU)" }, + */ + { "na.newnet.net", "NewNet (North America)" }, + { "eu.newnet.net", "NewNet (Europe)" }, + { "us.undernet.org", "Undernet (US)" }, + { "eu.undernet.org", "Undernet (Europe)" }, + { "irc.freenode.org", "FreeNode" }, + { "kubrick.freenode.net", "FreeNode (US)" }, + { "irc.efnet.org", "Eris Free Net" }, + { "82.165.139.95", "irc.tobug.net (NewNet, US)" }, +}; + +/* uIP API stuff */ +struct timer nick_reg_timer; /* ../uip/timer.h */ +struct timer vbell_timer; +struct telnet_state *tstate; /* ../apps/telnet.h */ + +/*---------------------------------------------------------------------------*/ +int main(void) { + char c; + int i; + uip_ipaddr_t ipaddr; + struct timer periodic_timer; + char cmdbuf[HOSTLEN+1]; + + c = get_ostype() & AT_OS_TYPE_MAIN; + if(c == AT_OS_400800) { + strcpy(os_version, "800"); + } else if(c == AT_OS_1200XL) { + strcpy(os_version, "1200XL"); + } + + cursor(1); /* don't let conio hide the cursor */ + + /* set screen colors to white-on-black initially. They may be + reset by the user's config file, once it's loaded */ + POKE(710, 0); + POKE(709, 12); + + putchar(A_CLR); + puts(BANNER); + putchar('\n'); + + get_config(); + + while(1) { + done = 0; + rs232dev_close(); + + POKE(710, config.bg_color); + POKE(709, config.fg_color); + + printf("Server name/IP, [C]onfig, or e[X]it\n"); + printf("[%s]: ", config.server); + fflush(stdout); + get_line(cmdbuf, HOSTLEN); + + if(strcasecmp(cmdbuf, "x") == 0) { + exit(0); + } else if(strcasecmp(cmdbuf, "c") == 0) { + config_menu(); + continue; + } else if(*cmdbuf) { + strcpy(config.server, cmdbuf); + } + /* else, use the existing config.server */ + + timer_set(&periodic_timer, CLOCK_SECOND / 2); + + rs232dev_init(config.baud); + uip_init(); + + uip_sethostaddr(config.local_ip); + uip_setdraddr(config.peer_ip); + + /* can I use 255.255.255.252 here? Does it matter? */ + uip_ipaddr(ipaddr, 255,255,255,0); + uip_setnetmask(ipaddr); + + if(uiplib_ipaddrconv(config.server, (unsigned char*)&ipaddr)) { + (void)uip_connect(&ipaddr, htons(config.server_port)); + } else { + resolv_init(); + resolv_conf(config.resolver_ip); + resolv_query(config.server); + } + + while(!done) { + /* This part of the while loop is straight from the no-ARP example + code, from the uIP docs. */ + uip_len = rs232dev_poll(); + if(uip_len > 0) { + uip_input(); + /* If the above function invocation resulted in data that + should be sent out on the network, the global variable + uip_len is set to a value > 0. */ + if(uip_len > 0) { + rs232dev_send(); + } + } else if(timer_expired(&periodic_timer)) { + timer_reset(&periodic_timer); + for(i = 0; i < UIP_CONNS; i++) { + uip_periodic(i); + /* If the above function invocation resulted in data that + should be sent out on the network, the global variable + uip_len is set to a value > 0. */ + if(uip_len > 0) { + rs232dev_send(); + } + } + +#if UIP_UDP + for(i = 0; i < UIP_UDP_CONNS; i++) { + uip_udp_periodic(i); + /* If the above function invocation resulted in data that + should be sent out on the network, the global variable + uip_len is set to a value > 0. */ + if(uip_len > 0) { + rs232dev_send(); + } + } +#endif /* UIP_UDP */ + } + + if(vbell_active && timer_expired(&vbell_timer)) { + POKE(710, config.bg_color); + vbell_active = 0; + } + + /* Call the keyboard handler if the user pressed a key. Note that + we're using stdio for printing, but conio for reading, which + works on the Atari but may not work on all other cc65 platforms */ + if(kbhit()) { + handle_keystroke(); + } + + /* Automatically register the nick and userhost after being + connected for a couple seconds. The parameters for the USER + command are ignored by the server (at least the NewNet servers) */ + if(connected && !nick_registered && timer_expired(&nick_reg_timer)) { + puts("Registering nick"); + sprintf(input_buf, "NICK %s%cUSER %s %s %s :%s%c", + config.nick, NL, config.nick, SELF, + config.server, config.real_name, NL); + telnet_send(tstate, input_buf, strlen(input_buf)); + input_buf_len = 0; + nick_registered = 1; + /* TODO: check for failure registering the nick */ + } + + /* + if(PEEK(53279) == 6) { // START pressed + puts("Disconnecting"); + uip_close(); + // done = 1; + } + */ + } + + connected = nick_registered = joined_channel = 0; + } + return 0; +} + +static void bell() { + if(config.ui_flags & UIFLAG_NOBELL) + return; + + if(config.ui_flags & UIFLAG_VBELL) { + vbell_active = 1; + timer_set(&vbell_timer, CLOCK_SECOND / 10); + POKE(710, config.bg_color + 8); + } else { + putchar(A_BEL); + } +} + +/* The err_* functions will probably support "visual bell" (screen flashing) + as an option one day. */ +static void err_missing_arg(void) { + puts("Command requires argument"); + bell(); +} + +static void err_no_channel(void) { + puts("You are not in a channel (use /join #channel)"); + bell(); +} + +static void err_already_joined(void) { + puts("You are already in a channel (use /part to leave)"); + bell(); +} + +static void del_last_word(void) { + int old = input_buf_len, bs = 0; + + while(input_buf_len && input_buf[input_buf_len] == ' ') { + ++bs; + input_buf_len--; + } + + while(input_buf_len && input_buf[input_buf_len] != ' ') { + ++bs; + input_buf_len--; + } + + if(old > 120 && input_buf_len <= 120) { + putchar(0x1c); + putchar(A_DEL); + putchar(A_DEL); + redraw_user_buffer(); + } else do { + putchar(A_BS); + } while(--bs); +} + +/* Keyboard handler. For now, the keyboard macros are hard-coded. */ +static void handle_keystroke(void) { + char i, c, send_buf = 0; + + /* TODO: + ctrl-7 = ` (warning: cgetc() can't read this) + ctrl-, = { + ctrl-. = } + ctrl-; = ~ (don't do ctrl-^, that's left-arrow!) + + Ignore inverse-video key, ATASCII graphics, unwanted + cursor actions, make caps-lock stop freezing the program. + To do this right, we have to examine location 764 for some + of these, and don't call cgetc() at all... or actually, + stuff a valid keycode there, then call cgetc() to make a + keyclick noise (but ignore its return value) + + Make Break act like delete-line + + */ + c = cgetc(); + + /* Keyboard macros, only allowed at the start of a new line */ + if(input_buf_len == 0) { + switch(c) { + case 0x0e: /* ctrl-N */ + if(joined_channel) send_server_cmd("NAMES", channel); + return; + + case 0x17: /* ctrl-W */ + if(joined_channel) send_server_cmd("WHO", channel); + return; + + case A_EOL: + return; /* ignore Return on a line by itself */ + + case A_TAB: + if(last_msg_nick[0]) { + sprintf(input_buf, "/msg %s ", last_msg_nick); + input_buf_len = strlen(input_buf); + for(i=0; i<input_buf_len; i++) + putchar(input_buf[i] | 0x80); + } + return; + + default: + break; + } + } + + /* Editing keys */ + /* TODO: more editing keys. At minimum: + left/right arrows + ^W = delete last word + ^U = kill to start of line + ^K = kill to end of line + ^A = goto start of line + ^E = goto end of line + ^L = redraw input buffer + */ + if(c == A_BS) { /* backspace */ + if(input_buf_len > 0) { + input_buf_len--; + putchar(c); + } else { + bell(); + } + + return; + } else if(c == A_DEL || c == 0x15) { + /* shift-backspace or ^U (delete line) */ + input_buf_len = 0; + putchar(A_DEL); + return; + } else if(c == 0x17) { + /* ^W (delete word) */ + del_last_word(); + return; + } + + /* Echo keystroke */ + putchar(c | 0x80); /* inverse video for now */ + + if(c == A_EOL) { + c = NL; + send_buf = 1; + } else if(c == A_TAB) { + c = TAB; + } + + /* Store keystroke in input buffer */ + input_buf[input_buf_len++] = c; + + /* If line too long, ring the "margin" bell */ + if(input_buf_len > BUF_SIZE - 1) + if(!send_buf) { + bell(); + putchar(A_BS); + input_buf_len--; + } + + /* If we've got a complete line of input and user has pressed Return, + send it to the server */ + if(send_buf) { + input_buf[input_buf_len] = '\0'; + + if(*input_buf == '/') { + handle_command(); + } else if(joined_channel) { + sprintf(chan_msg_buf, "PRIVMSG %s :%s", channel, input_buf); + telnet_send(tstate, chan_msg_buf, strlen(chan_msg_buf)); + } else { + err_no_channel(); + } + + input_buf_len = 0; + } +} + +static void send_server_cmd(char *cmd, char *arg) { + if(arg) { + sprintf(chan_msg_buf, "%s %s%c", cmd, arg, NL); + } else { + sprintf(chan_msg_buf, "%s%c", cmd, NL); + } + + telnet_send(tstate, chan_msg_buf, strlen(chan_msg_buf)); +} + +static void send_ctcp_version(char *arg) { + sprintf(chan_msg_buf, "PRIVMSG %s %cVERSION%c%c", + arg, 1, 1, NL); + telnet_send(tstate, chan_msg_buf, strlen(chan_msg_buf)); +} + +static void send_ctcp_ping(char *arg) { + sprintf(chan_msg_buf, "PRIVMSG %s %cPING %c%c", + arg, 1, 1, NL); + telnet_send(tstate, chan_msg_buf, strlen(chan_msg_buf)); +} + +static void do_me(char *arg) { /* Do me, baby! */ + sprintf(chan_msg_buf, "PRIVMSG %s :%cACTION %s%c%c", + channel, 1, arg, 1, NL); + telnet_send(tstate, chan_msg_buf, strlen(chan_msg_buf)); +} + +static void handle_command(void) { + char *cmd = input_buf + 1, *cend; + char *arg = NULL, *p = cmd; + + /* convert command word to uppercase */ + while((*p != NL) && (*p != ' ')) { + *p = toupper(*p); + ++p; + } + + cend = p; + + /* set arg pointer, nul-terminate arg, if present */ + /* (otherwise, arg remains NULL) */ + if(*p != NL) { + arg = p; + /* skip extra whitespace */ + while(*arg == ' ') + ++arg; + p = arg + 1; + while(*p && (*p != NL)) + ++p; + *p = '\0'; + } + + /* nul-terminate cmd */ + *cend = '\0'; + + /* See if it's a command that needs client-side help */ + if(strcmp(cmd, "ME") == 0) { + if(!joined_channel) + err_no_channel(); + else if(!arg) + err_missing_arg(); + else + do_me(arg); + } else if(strcmp(cmd, "MSG") == 0 || strcmp(cmd, "M") == 0) { + if(!arg) + err_missing_arg(); + else + send_server_cmd("PRIVMSG", arg); + } else if(strcmp(cmd, "JOIN") == 0 || strcmp(cmd, "J") == 0) { + if(joined_channel) { + err_already_joined(); + } else { + joined_channel = 1; + strcpy(channel, arg); + send_server_cmd("JOIN", arg); + } + } else if(strcmp(cmd, "PART") == 0) { + if(!joined_channel) { + err_no_channel(); + } else { + joined_channel = 0; + send_server_cmd("PART", channel); + } + } else if(strcmp(cmd, "VERSION") == 0 || strcmp(cmd, "VER") == 0) { + if(!arg) + err_missing_arg(); + else + send_ctcp_version(arg); + } else if(strcmp(cmd, "PING") == 0) { + if(!arg) + err_missing_arg(); + else + send_ctcp_ping(arg); + } else if(strcmp(cmd, "QUOTE") == 0) { + if(!arg) + err_missing_arg(); + else + send_server_cmd(arg, NULL); + } else { + /* Anything we don't recognize as a client command, we pass + directly to the server. */ + send_server_cmd(cmd, arg); + } +} + +/* The telnet_* functions are uIP application callbacks. */ +void telnet_connected(struct telnet_state *s) { + /* puts("Connected to host, press START to disconnect"); */ + puts("Connected to server"); + tstate = s; + s->text = NULL; + s->textlen = 0; + timer_set(&nick_reg_timer, CLOCK_SECOND); + connected = 1; +} + +void telnet_closed(struct telnet_state *s) { + puts("Connection closed"); + uip_close(); + done = 1; +} + +void telnet_sent(struct telnet_state *s) { +} + +void telnet_aborted(struct telnet_state *s) { + puts("Connection aborted"); + uip_abort(); + done = 1; +} + +void telnet_timedout(struct telnet_state *s) { + puts("Connection timed out"); + uip_abort(); + done = 1; +} + +void do_pong() { + char *p = chan_msg_buf; + + memcpy(chan_msg_buf, output_buf, output_buf_len); + chan_msg_buf[1] = 'O'; + while(*p) { + if(*p == A_EOL) *p = NL; + p++; + } + + telnet_send(tstate, chan_msg_buf, output_buf_len); + if(config.ui_flags & UIFLAG_SHOWPING) + puts("[PING,PONG]"); +} + +/* Handler for text received from the server, responsible for + formatting. TODO: part/join/quit aren't handled correctly. */ +void do_msg() { + char *nick = output_buf, *cmd = output_buf, *chan = NULL, *msg = NULL; + char *bang = NULL; + + while(*cmd != ' ') { + if(*cmd == '!') { + bang = cmd; + } + ++cmd; + } + + /* no ! in nick means a server message or something, just print as-is */ + if(!bang) { + fputs(output_buf, stdout); + fflush(stdout); + return; + } + + *cmd = '\0'; + ++cmd; + chan = cmd; + + while(*chan != ' ') + ++chan; + *chan = '\0'; + ++chan; + msg = chan; + + if(*msg != A_EOL) { + while(*msg && *msg != ' ') + ++msg; + if(*msg) { + *msg = '\0'; + ++msg; + if(*msg == ':') ++msg; + } + } + + /* FIXME: This stuff is hairy, unmaintainable, and probably just wrong + (works OK with the 2 or 3 NewNet servers I test with, not tried + others). It's the result of too much coffee, and needs to be + redesigned. Heck, it needs to be designed in the first place! */ + if(strcmp(cmd, "PRIVMSG") == 0) { + nick[0] = '<'; + bang[0] = '>'; + bang[1] = '\0'; + if(strcasecmp(chan, config.nick) != 0) { + // privmsg, not to our nick, must be channel text + if(memcmp(msg, "\x01" "ACTION ", 8) == 0) { + nick++; + *bang = '\0'; + output_buf[output_buf_len - 2] = A_EOL; + output_buf[output_buf_len - 1] = '\0'; + printf("* %s %s", nick, msg + 8); + } else { + printf("%s %s", nick, msg); + } + } else { + // privmsg, is to our nick + if(memcmp(msg, "\x01" "PING ", 6) == 0) { + nick++; + *bang = '\0'; + output_buf[output_buf_len - 2] = '\0'; + sprintf(chan_msg_buf, "NOTICE %s :\x01PING %s\x01%c", + nick, msg + 6, 0x0a); + telnet_send(tstate, chan_msg_buf, strlen(chan_msg_buf)); + printf("* CTCP PING from %s\n", nick); + } else if(memcmp(msg, "\x01" "VERSION\x01", 9) == 0) { + nick++; + *bang = '\0'; + sprintf(chan_msg_buf, "NOTICE %s :\x01VERSION " + VERSION_REPLY " - running on " + "an Atari %s\x01%c", + nick, os_version, 0x0a); + telnet_send(tstate, chan_msg_buf, strlen(chan_msg_buf)); + printf("* CTCP VERSION from %s\n", nick); + } else { + printf("MSG: %s %s", nick, msg); + if(config.ui_flags & UIFLAG_MSGBELL) + bell(); + *bang = '\0'; + strcpy(last_msg_nick, nick + 1); + } + } + } else { + if(*msg) { + printf("%s %s %s %s", nick, cmd, chan, msg); + } else { + printf("%s %s %s", nick, cmd, chan); + } + } + + fflush(stdout); +} + +void del_user_buffer() { + /* delete user edit buffer */ + if(input_buf_len) { + putchar(A_DEL); + if(input_buf_len > 119) { + putchar(0x1c); /* up arrow */ + putchar(A_DEL); + } + } +} + +void redraw_user_buffer() { + int tlen; + static char redraw_buf[BUF_SIZE]; + /* reprint user edit buffer */ + if(input_buf_len) { + tlen = input_buf_len - 1; + do { + redraw_buf[tlen] = input_buf[tlen] | 0x80; + } while(tlen--); + redraw_buf[input_buf_len] = '\0'; + fputs(redraw_buf, stdout); + fflush(stdout); + } +} + +/* Another uIP app callback. This decides whether or not we've received + a complete message from the server (which may be split into multiple + packets, and may end in the middle of a packet) */ +void telnet_newdata(struct telnet_state *s, char *data, u16_t len) { + u16_t tlen = len; + char c, *t = data, buf_done = 0; + + while(tlen-- != 0) { + c = *t++; + if(c == NL) { // swallow NL + continue; + } else if(c == CR) { // CR => EOL + c = A_EOL; + buf_done = 1; + } else if(c == TAB) { // tab + c = A_TAB; + } else if(c == '{') { + c = '[' | 0x80; + } else if(c == '}') { + c = ']' | 0x80; + } else if(c == '~') { + c = '^' | 0x80; + } else if(c == 0x60) { // backtick + c = 0xa7; // inverse quote + } + + output_buf[output_buf_len++] = c; + if(output_buf_len >= OUTBUF_SIZE - 1) { + puts("[buffer overflow]\xfd"); + buf_done = 1; + break; + } + + if(buf_done) { + del_user_buffer(); + output_buf[output_buf_len] = '\0'; + + if(output_buf_len > 4 && (memcmp(output_buf, "PING", 4) == 0)) { + do_pong(); + } else if(output_buf[0] == ':') { + do_msg(); + } else { + fputs(output_buf, stdout); + fflush(stdout); + } + + redraw_user_buffer(); + output_buf_len = 0; + buf_done = 0; + } + } +} + +/* Load config file, offer the user a chance to change the config */ +static void get_config(void) { + // char filename[20]; + FILE *f = fopen(DEFAULT_CONF_FILE, "rb"); + + puts("Loading config from " DEFAULT_CONF_FILE); + + if(f) { + config_valid = + fread(&config, 1, sizeof(fuji_conf_t), f) == sizeof(fuji_conf_t); + fclose(f); + if(!config_valid) + puts("Config file is wrong size"); + } else { + puts("No config file found"); + } + + if(config_valid) { + if(memcmp(config.signature, CONF_SIGNATURE, 2) != 0) { + puts("Not a valid config file"); + config_valid = 0; + } else if(config.version != CONF_VERSION) { + puts("Config is for the wrong version of FujiChat, ignoring"); + config_valid = 0; + } else { + puts("Loaded OK"); + /* + printf("Loaded OK. Use defaults [Y/n]? "); + fflush(stdout); + fgets(input_buf, 10, stdin); + if(*input_buf == 'N' || *input_buf == 'n') + config_menu(); + */ + } + } + + if(!config_valid) + config_menu(); +} + +static void save_config(void) { + FILE *f = fopen(DEFAULT_CONF_FILE, "wb"); + if(f) { + fwrite(&config, sizeof(fuji_conf_t), 1, f); + fclose(f); + puts("Config saved to " DEFAULT_CONF_FILE); + } +} + +static void get_conf_color(char *name, char *color, char dflt) { + char buf[5]; + + printf("%s color [%d]: ", name, dflt); + fgets(buf, 5, stdin); + + if(!(*buf >= '0' && *buf <= '9')) + *color = dflt; + else + *color = atoi(buf); +} + +static void get_line(char *buf, unsigned char len) { + fgets(buf, len, stdin); + while(*buf) { + if(*buf == '\n') + *buf = '\0'; + ++buf; + } +} + +static char get_yesno(char *prompt, char dflt) { + char buf[5]; + + printf("%s [%c/%c]: ", + prompt, + (dflt ? 'Y' : 'y'), + (dflt ? 'n' : 'N')); + + fflush(stdout); + get_line(buf, 4); + switch(*buf) { + case 'y': + case 'Y': + return 1; + + case 'n': + case 'N': + return 0; + + default: + return dflt; + } +} + +static void get_ip_addr(char *prompt, char *dflt, uip_ipaddr_t *addr) { + char ok; + char buf[HOSTLEN+1]; + + do { + printf("%s IP address [%s]: ", prompt, dflt); + fflush(stdout); + get_line(buf, HOSTLEN); + if(!*buf) strcpy(buf, dflt); + ok = uiplib_ipaddrconv(buf, (unsigned char*)addr); + if(!ok) bell(); + } while(!ok); +} + +static char *format_ip(uip_ipaddr_t *ip) { + static char buf[20]; + u16_t *ipaddr = (u16_t *)ip; + sprintf(buf, "%d.%d.%d.%d", + htons(ipaddr[0]) >> 8, + htons(ipaddr[0]) & 0xff, + htons(ipaddr[1]) >> 8, + htons(ipaddr[1]) & 0xff); + return buf; +} + +void set_default_config(void) { + uip_ipaddr(&(config.local_ip), 192,168,0,2); + uip_ipaddr(&(config.peer_ip), 192,168,0,1); + uip_ipaddr(&(config.resolver_ip), 192,168,0,1); + strcpy(config.server, servers[0][0]); + strcpy(config.nick, DEFAULT_NICK); + strcpy(config.real_name, "FujiChat User"); + config.server_port = 6667; + config.ui_flags = 0; + config.bg_color = 0xc0; /* dark green */ + config.fg_color = 0x0c; + config.baud = RS_BAUD_4800; + + config_valid = 1; +} + +int baud_values[] = { 1200, 2400, 4800, 9600 }; +char baud_bytes[] = { RS_BAUD_1200, RS_BAUD_2400, RS_BAUD_4800, RS_BAUD_9600 }; +char max_baud = sizeof(baud_bytes); + +static void get_baudrate(void) { + char i, buf[5]; + + for(i=0; i<max_baud; ++i) + if(config.baud == baud_bytes[i]) + printf("[%d]=%d ", i+1, baud_values[i]); + else + printf("%d=%d ", i+1, baud_values[i]); + + fputs("Baud rate: ", stdout); + fflush(stdout); + + do { + get_line(buf, 4); + if(!*buf) + return; + + i = atoi(buf); + } while (i < 1 || i > max_baud); + config.baud = baud_bytes[--i]; +} + +static void config_menu(void) { + char i, buf[HOSTLEN+1], ok = 0; + unsigned int new_value; + + strcpy(config.signature, CONF_SIGNATURE); + config.version = CONF_VERSION; + + if(!config_valid) + set_default_config(); + + do { + get_baudrate(); + get_ip_addr("Local", format_ip(&(config.local_ip)), &(config.local_ip)); + get_ip_addr("Peer", format_ip(&(config.peer_ip)), &(config.peer_ip)); + get_ip_addr("DNS Server", format_ip(&(config.resolver_ip)), &(config.resolver_ip)); + + puts("Server list:"); + for(i=0; i<SERVER_LIST_LEN; ++i) { + printf("%d: %s\n %s\n", + i + 1, + servers[i][0], + servers[i][1]); + } + + printf("\nEnter # or any server host/IP [%s]: ", config.server); + fflush(stdout); + fgets(buf, HOSTLEN, stdin); + + if(buf[0] == '\n') { + buf[0] = '1'; + buf[1] = '\n'; + } + + if(buf[0] >= '1' && buf[0] <= (SERVER_LIST_LEN + '0') && buf[1] == '\n') { + strcpy(buf, servers[buf[0] - '1'][0]); + } + + for(i=0; buf[i] && (buf[i] != '\n'); ++i) + ; + + buf[i] = '\0'; + strcpy(config.server, buf); + + printf("Server port [%d]: ", config.server_port); + get_line(buf, 10); + new_value = atoi(buf); + if(new_value) config.server_port = new_value; + + printf("Your nickname [%s]: ", config.nick); + get_line(buf, NICKLEN); + if(*buf) strcpy(config.nick, buf); + + printf("Your 'real' name [%s]: ", config.real_name); + get_line(buf, NICKLEN); + if(*buf) strcpy(config.real_name, buf); + + // if(!*(config.nick)) strcpy(config.nick, DEFAULT_NICK); + /* TODO: scan for invalid chars */ + + get_conf_color("Background", &(config.bg_color), config.bg_color); + get_conf_color("Foreground", &(config.fg_color), config.fg_color); + + new_value = + (get_yesno("Disable bell", config.ui_flags & UIFLAG_NOBELL) ? UIFLAG_NOBELL : 0); + + if(!(config.ui_flags & UIFLAG_NOBELL)) { + new_value |= + (get_yesno("Visual bell", config.ui_flags & UIFLAG_VBELL) ? UIFLAG_VBELL : 0); + + new_value |= + (get_yesno("Bell on msg", config.ui_flags & UIFLAG_MSGBELL) ? UIFLAG_MSGBELL : 0); + }; + + new_value |= + (get_yesno("Show ping/pong", config.ui_flags & UIFLAG_SHOWPING) ? UIFLAG_SHOWPING : 0); + + config.ui_flags = new_value; + + ok = get_yesno("Is this correct", 1); + } while(!ok); + + if(get_yesno("Save this config", 1)) + save_config(); +} + +/*---------------------------------------------------------------------------*/ + +/* uIP app callback for the resolver */ +void resolv_found(char *name, u16_t *ipaddr) { + if(ipaddr == NULL) { + printf("Host '%s' not found.\n", name); + done = 1; + } else { + printf("Found name '%s' = %d.%d.%d.%d\n", name, + htons(ipaddr[0]) >> 8, + htons(ipaddr[0]) & 0xff, + htons(ipaddr[1]) >> 8, + htons(ipaddr[1]) & 0xff); + if(!connected) + (void)uip_connect((uip_ipaddr_t *)ipaddr, htons(config.server_port)); + } +} |