From 2fec6ab70dfe1108ed1d0c20465922e237c38f2f Mon Sep 17 00:00:00 2001 From: "garbeam@wmii.de" Date: Mon, 19 Dec 2005 15:39:54 +0200 Subject: [PATCH] added initial files --- FAQ | 19 +++ LICENSE | 22 +++ Makefile | 48 ++++++ README | 46 ++++++ config.mk | 18 ++ ii.1 | 95 +++++++++++ ii.c | 484 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 732 insertions(+) create mode 100644 FAQ create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 config.mk create mode 100644 ii.1 create mode 100644 ii.c diff --git a/FAQ b/FAQ new file mode 100644 index 0000000..3906c8b --- /dev/null +++ b/FAQ @@ -0,0 +1,19 @@ +FAQ + +Where is IRC command xy (ignore etc.)? +-------------------------------------- +ii is for advanced users, please use standard tools like awk, sed and grep for +this. This can be done easily and will not bloat the code. + +Where is a graphical interface? +------------------------------- +Basically ii follows the UNIX philosophie so it is only file based. But it +should be easy to build different interface because they only have to handle +the FIFOs and output files. Feel free to implement or wait until we have done +this. + +Which commands are supported? + ----------------------------- +j (join), t (topic), a (away), m (msg), n (nick), l (leave). The missing are +obsolete or can be easily used by typing the IRC commands itself (i.e. /WHO +instead of /who). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9176dbc --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT/X Consortium License + +(C)opyright MMV Anselm R. Garbe +(C)opyright MMV Nico Golde + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9bb0bd2 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +# ii - irc it - simple but flexible IRC client +# (C)opyright MMV Anselm R. Garbe, Nico Golde + +include config.mk + +SRC = ii.c +OBJ = ${SRC:.c=.o} + +all: options ii + @echo built ii + +options: + @echo ii build options: + @echo "LIBS = ${LIBS}" + @echo "INCLUDES = ${INCLUDES}" + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +.c.o: + @echo CC $< + @${CC} -c ${CFLAGS} $< + +dist: clean + @mkdir -p ii-${VERSION} + @cp -R Makefile README FAQ LICENSE config.mk ii.c ii.1 ii-${VERSION} + @tar -cf ii-${VERSION}.tar ii-${VERSION} + @gzip ii-${VERSION}.tar + @rm -rf ii-${VERSION} + @echo created distribution ii-${VERSION}.tar.gz + +ii: ${OBJ} + @echo LD $@ + @${CC} -o $@ ${OBJ} ${LDFLAGS} + +install: all + @cp ii ${DESTDIR}${PREFIX}/bin + @chmod 775 ${DESTDIR}${PREFIX}/bin/ii + @cp ii.1 ${DESTDIR}${MANPREFIX}/man1 + @chmod 444 ${DESTDIR}${MANPREFIX}/man1/ii.1 + @echo "installed ii" + +uninstall: all + rm -f ${DESTDIR}${MANPREFIX}/man1/ii.1 + rm -f ${DESTDIR}${PREFIX}/bin/ii + +clean: + rm -f ii *~ *.o *core diff --git a/README b/README new file mode 100644 index 0000000..cedcae4 --- /dev/null +++ b/README @@ -0,0 +1,46 @@ +Abstract +-------- +ii is a minimalistic FIFO and filesystem based IRC client. +It creates an irc directory tree with server, channel and +nick name directories. +In every directory a FIFO file (in) and and normal file (out) +is placed. +The in file is used to communicate with the servers and the out +files includes the server messages. For every channel and every nick +name there will be new in and out files. +The basic idea of this is to be able to communicate with an IRC +server with basic command line tools. +For example if you will join a channel just do echo "/j #channel" > in +and ii creates a new channel directory with in and out file. + +Installation +------------ +Edit config.mk to match your local setup. ii is installed into +/usr/local by default. + +Afterwards enter the following command to build and install ii (if +necessary as root): + + $ make clean install + +Running ii +------------ +Simply invoke the 'ii' command with required arguments + +To make ii a bit more comfortable use it in combination with the multitail +program and for example with vim. Run vim in the server directory and use +key mapping like: +map w1 :.w >> \#ii/in +map w2 :.w >> \#wmii/in +to post to channels. Thanks to Matthias Kopfermann for this hint. + +Configuration +------------- +No configuration is needed. + +Contact +------- +If you want to contact the developers just write a mail to +ii (at) modprobe (dot) de + +--Nico Golde, Anselm R. Garbe diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..004c1cf --- /dev/null +++ b/config.mk @@ -0,0 +1,18 @@ +# Customize to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +INCDIR = ${PREFIX}/include +LIBDIR = ${PREFIX}/lib +VERSION = 1-rc1 + +# includes and libs +INCLUDES = -I. -I${INCDIR} -I/usr/include +LIBS = -L${LIBDIR} -L/usr/lib -lc + +# compiler +CC = cc +CFLAGS = -g -O0 -W -Wall ${INCLUDES} -DVERSION=\"${VERSION}\" +LDFLAGS = ${LIBS} diff --git a/ii.1 b/ii.1 new file mode 100644 index 0000000..5c5628c --- /dev/null +++ b/ii.1 @@ -0,0 +1,95 @@ +.de FN +\fI\|\\$1\|\fP\\$2 +.. +.TH ii 1 +.SH NAME +ii \- irc it or irc improved + +.SH DESCRIPTION +.B ii +is a minimalistic FIFO and filesystem based IRC client. +It creates an irc directory tree with server, channel and +nick name directories. +In every directory a FIFO file (in) and and normal file (out) +is placed. This will be for example ~/irc/irc.freenode.net/. +The in file is used to communicate with the servers and the out +files includes the server messages. For every channel and every nick +name there will be new in and out files. +The basic idea of this is to be able to communicate with an IRC +server with basic command line tools. +For example if you will join a channel just do echo "/j #channel" > in +and ii creates a new channel directory with in and out file. +.SH SYNOPSIS +.B ii +.RB [ \-s +.IR servername ] +.RB [ \-p +.IR port ] +.RB [ \-k +.IR password ] +.RB [ \-i +.IR prefix ] +.RB [ \-n +.IR nickname ] +.RB [ \-f +.IR realname ] +.RB \-v + +.SH OPTIONS +.TP +.BI \-s " servername" +lets you override the default servername (irc.freenode.net) +.TP +.BI \-p " port" +lets you override the default port (6667) +.TP +.BI \-k " password" +lets you use a password to authentifacte your nick on the server +.TP +.BI \-i " prefix" +lets you override the default irc path (~/irc) +.TP +.BI \-n " nickname" +lets you override the default nick ($USER) +.TP +.BI \-f " realname" +lets you specify your real name associated with your nick + +.SH DIRECTORIES +.TP +.FN ~/irc +In this directory the irc tree will be created. In this directory you +will find a directory for your server (default: irc.freenode.net) in +which the FIFO and the output file will be stored. +If you join a channel a new directory with the name of the channel +will be created in the ~/irc/$servername/ directory. + +.SH COMMANDS +.TP +.FN /j " #channel" +join a channel +.TP +.FN /l " #channel" +leave a channel +.TP +.FN /m " user msg" +write a private message to #user +.TP +.FN /n " nick" +change the nick name +.TP +.FN /t " topic" +set the topic of a channel +.TP +Everything which is not a command will simply be posted into the channel or to the server. +.TP +.FH out file usage +Write wrappers, pagers or use your tools of choice to display the out file contents (loco, multitail, etc.). +.SH CONTACT +.TP +Write to ii (at) modprobe (dot) de for suggestions, fixes, 7|-|>< ;) etc. +.SH AUTHORS +Copyright \(co 2005 by Anselm R. Garbe and Nico Golde +.SH SEE ALSO +.BR echo (1), +.BR tail (1), diff --git a/ii.c b/ii.c new file mode 100644 index 0000000..700bf0d --- /dev/null +++ b/ii.c @@ -0,0 +1,484 @@ +/* + * (C)opyright MMV Anselm R. Garbe + * Nico Golde + * See LICENSE file for license details. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef nil +#define nil NULL /* for those who don't understand, nil is used in Plan9 */ +#endif + +enum { TOK_NICKSRV = 0, TOK_USER, TOK_CMD, TOK_CHAN, TOK_ARG, TOK_TEXT, TOK_LAST }; + +static int irc; +static char *fifo[256]; +static char *server = "irc.freenode.net"; +static char nick[32]; /* might change while running */ +static char path[_POSIX_PATH_MAX]; +static char buf[PIPE_BUF]; /* buffers used for communication */ +static char _buf[PIPE_BUF]; /* help buffer */ + +static int add_channel(char *channel); + +static void usage() +{ + fprintf(stderr, "%s", + "ii - irc it - " VERSION "\n" + " (C)opyright MMV Anselm R. Garbe, Nico Golde\n" + "usage: ii [-i ] [-s ] [-p ]\n" + " [-n ] [-k ] [-f ]\n"); + exit(EXIT_SUCCESS); +} + +static void login(char *key, char *fullname) +{ + if(key) + snprintf(buf, PIPE_BUF, + "PASS %s\r\nNICK %s\r\nUSER %s localhost %s :%s\r\n", key, + nick, nick, server, fullname ? fullname : nick); + else + snprintf(buf, PIPE_BUF, "NICK %s\r\nUSER %s localhost %s :%s\r\n", + nick, nick, server, fullname ? fullname : nick); + write(irc, buf, strlen(buf)); /* login */ +} + +static int tcpopen(unsigned short port) +{ + int fd; + struct sockaddr_in sin; + struct hostent *hp = gethostbyname(server); + + memset(&sin, 0, sizeof(struct sockaddr_in)); + if(hp == nil) { + perror("ii: cannot retrieve host information"); + exit(EXIT_FAILURE); + } + sin.sin_family = AF_INET; + bcopy(hp->h_addr, (char *) &sin.sin_addr, hp->h_length); + sin.sin_port = htons(port); + if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { + perror("ii: cannot create socket"); + exit(EXIT_FAILURE); + } + if(connect(fd, (const struct sockaddr *) &sin, sizeof(sin)) < 0) { + perror("ii: cannot connect to server"); + exit(EXIT_FAILURE); + } + return fd; +} + +static size_t tokenize(char **result, size_t reslen, char *str, char delim) +{ + char *p, *n; + size_t i; + + if(!str) + return 0; + for(n = str; *n == ' '; n++); + p = n; + for(i = 0; *n != 0;) { + if(i == reslen) + return 0; + if(*n == delim) { + *n = 0; + result[i++] = p; + p = ++n; + } else + n++; + } + result[i++] = p; + return i + 2; /* number of tokens */ +} + +/* creates directories top-down, if necessary */ +static void _mkdir(const char *dir) +{ + char tmp[256]; + char *p; + size_t len; + + snprintf(tmp, sizeof(tmp),"%s",dir); + len = strlen(tmp); + if(tmp[len - 1] == '/') + tmp[len - 1] = 0; + for(p = tmp + 1; *p; p++) + if(*p == '/') { + *p = 0; + if(access(tmp, F_OK)) + mkdir(tmp, S_IRWXU); + *p = '/'; + } + if(access(tmp, F_OK)) + mkdir(tmp, S_IRWXU); +} + +static int _create_filepath(char *filepath, size_t len, char *channel, + char *file) +{ + if(channel) { + if(!snprintf(filepath, len, "%s/%s", path, channel)) + return 0; + _mkdir(filepath); + return snprintf(filepath, len, "%s/%s/%s", path, channel, file); + } + return snprintf(filepath, len, "%s/%s", path, file); +} + +static void create_filepath(char *filepath, size_t len, char *channel, + char *suffix) +{ + if(!_create_filepath(filepath, len, channel, suffix)) { + fprintf(stderr, "%s", "ii: path to irc directory too long\n"); + exit(EXIT_FAILURE); + } +} + +static void print_out(char *channel, char *buffer) +{ + static char outfile[256]; + FILE *out; + static char buft[8]; + time_t t = time(0); + + create_filepath(outfile, sizeof(outfile), channel, "out"); + out = fopen(outfile, "a"); + strftime(buft, sizeof(buft), "%R", localtime(&t)); + fprintf(out, "%s %s\n", buft, buffer); + fclose(out); +} + +static void proc_fifo_privmsg(char *channel, char *buffer) +{ + snprintf(buf, PIPE_BUF, "<%s> %s", nick, buffer); + print_out(channel, buf); + snprintf(buf, PIPE_BUF, "PRIVMSG %s :%s\r\n", channel, buffer); + write(irc, buf, strlen(buf)); +} + +static void proc_fifo_input(int fd, char *buffer) +{ + static char infile[256]; + char *p; + /*int ret = 1; */ + if(buffer[0] != '/') { + if(fifo[fd][0] != 0) + proc_fifo_privmsg(fifo[fd], buffer); + return; + } + switch (buffer[1]) { + case 'j': + p = strchr(&buffer[3], ' '); + if(p) + *p = 0; + snprintf(buf, PIPE_BUF, "JOIN %s\r\n", &buffer[3]); + add_channel(&buffer[3]); + break; + case 't': + snprintf(buf, PIPE_BUF, "TOPIC %s :%s\r\n", fifo[fd], &buffer[3]); + break; + case 'a': + snprintf(buf, PIPE_BUF, "-!- %s is away \"%s\"", nick, &buffer[3]); + print_out(fifo[fd], buf); + snprintf(buf, PIPE_BUF, "AWAY :%s\r\n", &buffer[3]); + break; + case 'm': + p = strchr(&buffer[3], ' '); + if(p) { + *p = 0; + add_channel(&buffer[3]); + proc_fifo_privmsg(&buffer[3], p + 1); + } + return; + break; + case 'n': + snprintf(nick, sizeof(nick),"%s", buffer); + snprintf(buf, PIPE_BUF, "NICK %s\r\n", &buffer[3]); + break; + case 'l': + if(fifo[fd][0] == 0) + return; + if(buffer[2] == ' ') + snprintf(buf, PIPE_BUF, "PART %s :%s\r\n", fifo[fd], + &buffer[3]); + else + snprintf(buf, PIPE_BUF, + "PART %s :ii - 500SLOC are too much\r\n", fifo[fd]); + write(irc, buf, strlen(buf)); + close(fd); + create_filepath(infile, sizeof(infile), fifo[fd], "in"); + unlink(infile); + free(fifo[fd]); + fifo[fd] = 0; + return; + break; + default: + snprintf(buf, PIPE_BUF, "%s\r\n", &buffer[1]); + break; + } + write(irc, buf, strlen(buf)); +} + +static void proc_server_cmd(char *buffer) +{ + char *argv[TOK_LAST], *cmd, *p; + int i; + if(!buffer) + return; + + for(i = 0; i < TOK_LAST; i++) + argv[i] = nil; + + /* + ::= [':' ] + ::= | [ '!' ] [ '@' ] + ::= { } | + ::= ' ' { ' ' } + ::= [ ':' | ] + ::= + ::= + ::= CR LF + */ + if(buffer[0] == ':') { /* check prefix */ + p = strchr(buffer, ' '); + *p = 0; + for(++p; *p == ' '; p++); + cmd = p; + argv[TOK_NICKSRV] = &buffer[1]; + if((p = strchr(buffer, '!'))) { + *p = 0; + argv[TOK_USER] = ++p; + } + } else + cmd = buffer; + + /* remove CRLFs */ + for(p = cmd; p && *p != 0; p++) + if(*p == '\r' || *p == '\n') + *p = 0; + + if((p = strchr(cmd, ':'))) { + *p = 0; + argv[TOK_TEXT] = ++p; + } + tokenize(&argv[TOK_CMD], TOK_LAST - TOK_CMD + 1, cmd, ' '); + + if(!strncmp("PING", argv[TOK_CMD], 5)) { + snprintf(buf, PIPE_BUF, "PONG %s\r\n", argv[TOK_TEXT]); + write(irc, buf, strlen(buf)); + return; + } else if(!argv[TOK_NICKSRV] || !argv[TOK_USER]) { /* server command */ + snprintf(buf, PIPE_BUF, "%s", argv[TOK_TEXT] ? argv[TOK_TEXT] : ""); + print_out(0, buf); + return; + } else if(!strncmp("ERROR", argv[TOK_CMD], 6)) + snprintf(buf, PIPE_BUF, "-!- error %s", argv[TOK_TEXT] ? argv[TOK_TEXT] : "unknown"); + else if(!strncmp("JOIN", argv[TOK_CMD], 5)) { + if(argv[TOK_TEXT]!=nil){ + p = strchr(argv[TOK_TEXT], ' '); + if(p) + *p = 0; + } + argv[TOK_CHAN] = argv[TOK_TEXT]; + snprintf(buf, PIPE_BUF, "-!- %s(%s) has joined %s", argv[TOK_NICKSRV], argv[TOK_USER], argv[TOK_TEXT]); + } else if(!strncmp("PART", argv[TOK_CMD], 5)) { + snprintf(buf, PIPE_BUF, "-!- %s(%s) has left %s", argv[TOK_NICKSRV], argv[TOK_USER], argv[TOK_CHAN]); + } else if(!strncmp("MODE", argv[TOK_CMD], 5)) + snprintf(buf, PIPE_BUF, "-!- %s changed mode/%s -> %s %s", argv[TOK_NICKSRV], argv[TOK_CMD + 1], argv[TOK_CMD + 2], argv[TOK_CMD + 3]); + else if(!strncmp("QUIT", argv[TOK_CMD], 5)) + snprintf(buf, PIPE_BUF, "-!- %s(%s) has quit %s \"%s\"", argv[TOK_NICKSRV], argv[TOK_USER], argv[TOK_CHAN], argv[TOK_TEXT] ? argv[TOK_TEXT] : ""); + else if(!strncmp("NICK", argv[TOK_CMD], 5)) + snprintf(buf, PIPE_BUF, "-!- %s changed nick to %s", argv[TOK_NICKSRV], argv[TOK_TEXT]); + else if(!strncmp("TOPIC", argv[TOK_CMD], 6)) + snprintf(buf, PIPE_BUF, "-!- %s changed topic to \"%s\"", argv[TOK_NICKSRV], argv[TOK_TEXT] ? argv[TOK_TEXT] : ""); + else if(!strncmp("KICK", argv[TOK_CMD], 5)) + snprintf(buf, PIPE_BUF, "-!- %s kicked %s (\"%s\")", argv[TOK_NICKSRV], argv[TOK_ARG], argv[TOK_TEXT] ? argv[TOK_TEXT] : ""); + else if(!strncmp("NOTICE", argv[TOK_CMD], 7)) + snprintf(buf, PIPE_BUF, "-!- \"%s\")", argv[TOK_TEXT] ? argv[TOK_TEXT] : ""); + else if(!strncmp("PRIVMSG", argv[TOK_CMD], 8)) + snprintf(buf, PIPE_BUF, "<%s> %s", argv[TOK_NICKSRV], argv[TOK_TEXT] ? argv[TOK_TEXT] : ""); + if(!argv[TOK_CHAN] || !strncmp(argv[TOK_CHAN], nick, strlen(nick))) + print_out(argv[TOK_NICKSRV], buf); + else + print_out(argv[TOK_CHAN], buf); +} + +static int open_fifo(char *channel) +{ + static char infile[256]; + create_filepath(infile, sizeof(infile), channel, "in"); + if(access(infile, F_OK) == -1) + mkfifo(infile, S_IRWXU); + return open(infile, O_RDONLY | O_NONBLOCK, 0); +} + +static int add_channel(char *channel) +{ + int i; + char *new; + + if(channel && channel[0] != 0) { + for(i = 0; i < 256; i++) + if(fifo[i] && !strncmp(channel, fifo[i], strlen(channel))) + return 1; + } + new = strdup(channel); + if((i = open_fifo(new)) >= 0) + fifo[i] = new; + else { + perror("ii: cannot create in fifo"); + return 0; + } + return 1; +} + +static int readl_fd(int fd) +{ + int i = 0; + char c; + do { + if(read(fd, &c, sizeof(char)) != sizeof(char)) + return 0; + _buf[i++] = c; + } + while(c != '\n' && i 0) { + for(i = 0; i < 256; i++) { + if(FD_ISSET(i, &rd)) { + if(i == irc) + handle_server_output(); + else + handle_fifo_input(i); + } + } + } + } +} + +int main(int argc, char *argv[]) +{ + int i; + unsigned short port = 6667; + struct passwd *spw = getpwuid(getuid()); + char *key = nil; + char prefix[_POSIX_PATH_MAX]; + char *fullname = nil; + + if(!spw) { + fprintf(stderr,"ii: getpwuid() failed\n"); + exit(EXIT_FAILURE); + } + snprintf(nick, sizeof(nick), "%s", spw->pw_name); + snprintf(prefix, sizeof(prefix),"%s", spw->pw_dir); + + if(argc == 2 && argv[1][0] == '-' && argv[1][1] == 'h') + usage(); + + for(i = 1; (i + 1 < argc) && (argv[i][0] == '-'); i++) { + switch (argv[i][1]) { + case 'i': + snprintf(prefix,sizeof(prefix),"%s", argv[++i]); + break; + case 's': + server = argv[++i]; + break; + case 'p': + port = atoi(argv[++i]); + break; + case 'n': + snprintf(nick,sizeof(nick),"%s", argv[++i]); + break; + case 'k': + key = argv[++i]; + break; + case 'f': + fullname = argv[++i]; + break; + default: + usage(); + break; + } + } + irc = tcpopen(port); + if(!snprintf(path, sizeof(path), "%s/irc/%s", prefix, server)) { + fprintf(stderr, "%s", "ii: path to irc directory too long\n"); + exit(EXIT_FAILURE); + } + _mkdir(path); + + for(i = 0; i < 256; i++) + fifo[i] = 0; + + if(!add_channel("")) + exit(EXIT_FAILURE); + login(key, fullname); + run(); + + return 0; +}