mirror of https://github.com/nealey/irc-bot
480 lines
10 KiB
C
480 lines
10 KiB
C
#include <stdio.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <ctype.h>
|
|
#include <time.h>
|
|
#include <errno.h>
|
|
#include <sysexits.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
|
|
#include "dump.h"
|
|
|
|
#define MAX_ARGS 50
|
|
#define MAX_SUBPROCS 50
|
|
|
|
#define max(a,b) ((a)>(b)?(a):(b))
|
|
|
|
bool running = true;
|
|
char *handler = NULL;
|
|
char *msgdir = NULL;
|
|
struct timeval output_interval = {0};
|
|
|
|
void
|
|
maybe_setenv(char *key, char *val)
|
|
{
|
|
if (val) {
|
|
setenv(key, val, 1);
|
|
}
|
|
}
|
|
|
|
void
|
|
irc_filter(const char *str)
|
|
{
|
|
char buf[4096];
|
|
char *line = buf;
|
|
char *parts[20] = {0};
|
|
int nparts;
|
|
char snick[20];
|
|
char *cmd;
|
|
char *text = NULL;
|
|
char *prefix = NULL;
|
|
char *sender = NULL;
|
|
char *forum = NULL;
|
|
int i;
|
|
|
|
strncpy(buf, str, sizeof buf);
|
|
/* Tokenize IRC line */
|
|
nparts = 0;
|
|
if (':' == *line) {
|
|
prefix = line + 1;
|
|
} else {
|
|
parts[nparts++] = line;
|
|
}
|
|
while (*line) {
|
|
if (' ' == *line) {
|
|
*line++ = '\0';
|
|
if (':' == *line) {
|
|
text = line+1;
|
|
break;
|
|
} else {
|
|
parts[nparts++] = line;
|
|
}
|
|
} else {
|
|
line += 1;
|
|
}
|
|
}
|
|
|
|
/* Strip trailing carriage return */
|
|
while (*line) line += 1;
|
|
if ('\r' == *(line-1)) *(line-1) = '\0';
|
|
|
|
/* Set command, converting to upper case */
|
|
cmd = parts[0];
|
|
for (i = 0; cmd[i]; i += 1) {
|
|
cmd[i] = toupper(cmd[i]);
|
|
}
|
|
|
|
/* Extract prefix nickname */
|
|
for (i = 0; prefix && (prefix[i] != '!'); i += 1) {
|
|
if (i == sizeof(snick) - 1) {
|
|
i = 0;
|
|
break;
|
|
}
|
|
snick[i] = prefix[i];
|
|
}
|
|
snick[i] = '\0';
|
|
if (i) {
|
|
sender = snick;
|
|
}
|
|
|
|
/* Determine forum */
|
|
if ((0 == strcmp(cmd, "PRIVMSG")) ||
|
|
(0 == strcmp(cmd, "NOTICE"))) {
|
|
/* :neale!user@127.0.0.1 PRIVMSG #hydra :foo */
|
|
switch (parts[1][0]) {
|
|
case '#':
|
|
case '&':
|
|
case '+':
|
|
case '!':
|
|
forum = parts[1];
|
|
break;
|
|
default:
|
|
forum = snick;
|
|
break;
|
|
}
|
|
} else if ((0 == strcmp(cmd, "PART")) ||
|
|
(0 == strcmp(cmd, "MODE")) ||
|
|
(0 == strcmp(cmd, "TOPIC")) ||
|
|
(0 == strcmp(cmd, "KICK"))) {
|
|
forum = parts[1];
|
|
} else if (0 == strcmp(cmd, "JOIN")) {
|
|
if (0 == nparts) {
|
|
forum = text;
|
|
text = NULL;
|
|
} else {
|
|
forum = parts[1];
|
|
}
|
|
} else if (0 == strcmp(cmd, "INVITE")) {
|
|
forum = text?text:parts[2];
|
|
text = NULL;
|
|
} else if (0 == strcmp(cmd, "NICK")) {
|
|
sender = parts[1];
|
|
forum = sender;
|
|
} else if (0 == strcmp(cmd, "PING")) {
|
|
printf("PONG :%s\r\n", text);
|
|
fflush(stdout);
|
|
}
|
|
|
|
{
|
|
int _argc;
|
|
char *_argv[MAX_ARGS + 1];
|
|
|
|
maybe_setenv("handler", handler);
|
|
maybe_setenv("prefix", prefix);
|
|
maybe_setenv("command", cmd);
|
|
maybe_setenv("sender", sender);
|
|
maybe_setenv("forum", forum);
|
|
maybe_setenv("text", text);
|
|
|
|
_argc = 0;
|
|
_argv[_argc++] = handler;
|
|
for (i = 1; (i < nparts) && (_argc < MAX_ARGS); i += 1) {
|
|
_argv[_argc++] = parts[i];
|
|
}
|
|
_argv[_argc] = NULL;
|
|
|
|
execvp(handler, _argv);
|
|
perror(handler);
|
|
}
|
|
}
|
|
|
|
void
|
|
unblock(int fd)
|
|
{
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
|
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
}
|
|
|
|
FILE *subprocs[MAX_SUBPROCS] = { 0 };
|
|
|
|
void
|
|
sigchld(int signum)
|
|
{
|
|
while (0 < waitpid(-1, NULL, WNOHANG));
|
|
}
|
|
|
|
|
|
void
|
|
dispatch(char *text)
|
|
{
|
|
int subout[2];
|
|
int i;
|
|
|
|
for (i = 0; i < MAX_SUBPROCS; i += 1) {
|
|
if (NULL == subprocs[i]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (MAX_SUBPROCS == i) {
|
|
fprintf(stderr, "warning: dropping message (too many children)\n");
|
|
return;
|
|
}
|
|
|
|
if (-1 == pipe(subout)) {
|
|
perror("pipe");
|
|
return;
|
|
}
|
|
|
|
subprocs[i] = fdopen(subout[0], "r");
|
|
if (! subprocs[i]) {
|
|
close(subout[0]);
|
|
close(subout[1]);
|
|
perror("fdopen");
|
|
return;
|
|
}
|
|
|
|
if (0 == fork()) {
|
|
/*
|
|
* Child
|
|
*/
|
|
int null;
|
|
|
|
if ((-1 == (null = open("/dev/null", O_RDONLY))) ||
|
|
(-1 == dup2(null, 0)) ||
|
|
(-1 == dup2(subout[1], 1))) {
|
|
perror("fd setup");
|
|
exit(EX_OSERR);
|
|
}
|
|
|
|
/*
|
|
* We'll be a good citizen and only close file descriptors we opened.
|
|
*/
|
|
close(null);
|
|
close(subout[1]);
|
|
for (i = 0; i < MAX_SUBPROCS; i += 1) {
|
|
if (subprocs[i]) {
|
|
fclose(subprocs[i]);
|
|
}
|
|
}
|
|
|
|
irc_filter(text);
|
|
exit(0);
|
|
}
|
|
|
|
unblock(subout[0]);
|
|
close(subout[1]);
|
|
}
|
|
|
|
void
|
|
delay_output()
|
|
{
|
|
struct timeval now;
|
|
struct timeval diff;
|
|
static struct timeval output_last = { 0 };
|
|
|
|
gettimeofday(&now, NULL);
|
|
timersub(&now, &output_last, &diff);
|
|
if (timercmp(&diff, &output_interval, <)) {
|
|
struct timeval delay;
|
|
struct timespec ts;
|
|
int ret;
|
|
|
|
timersub(&output_interval, &diff, &delay);
|
|
|
|
ts.tv_sec = (time_t) delay.tv_sec;
|
|
ts.tv_nsec = (long) (delay.tv_usec * 1000);
|
|
do {
|
|
ret = nanosleep(&ts, &ts);
|
|
} while ((-1 == ret) && (EINTR == errno));
|
|
gettimeofday(&output_last, NULL);
|
|
} else {
|
|
output_last = now;
|
|
}
|
|
}
|
|
|
|
|
|
/** Writes all of buf to stdout, possibly blocking. */
|
|
void
|
|
output(char *buf)
|
|
{
|
|
if (timerisset(&output_interval)) {
|
|
delay_output();
|
|
}
|
|
|
|
puts(buf);
|
|
}
|
|
|
|
void
|
|
handle_file(FILE *f, void (*func) (char *))
|
|
{
|
|
char line[2048];
|
|
size_t linelen;
|
|
|
|
// Read a line. If we didn't have enough space, drop it.
|
|
while (fgets(line, sizeof line, f)) {
|
|
linelen = strlen(line);
|
|
if (line[linelen-1] != '\n') {
|
|
fprintf(stderr, "warning: dropping %d bytes (no trailing newline)\n", linelen);
|
|
} else {
|
|
line[linelen-1] = '\0';
|
|
func(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
handle_input()
|
|
{
|
|
handle_file(stdin, dispatch);
|
|
if (feof(stdin)) {
|
|
running = false;
|
|
}
|
|
}
|
|
|
|
void
|
|
handle_subproc(FILE *s)
|
|
{
|
|
handle_file(s, output);
|
|
}
|
|
|
|
void
|
|
loop()
|
|
{
|
|
int i;
|
|
int ret;
|
|
int nfds = 0;
|
|
fd_set rfds;
|
|
static time_t last_pulse = 0;
|
|
time_t now;
|
|
|
|
// Look for messages in msgdir
|
|
if (msgdir) {
|
|
DIR *d = opendir(msgdir);
|
|
|
|
while (d) {
|
|
struct dirent *ent = readdir(d);
|
|
|
|
if (! ent) {
|
|
break;
|
|
}
|
|
if (ent->d_type == DT_REG) {
|
|
char fn[PATH_MAX];
|
|
FILE *f;
|
|
|
|
snprintf(fn, sizeof fn, "%s/%s", msgdir, ent->d_name);
|
|
f = fopen(fn, "r");
|
|
if (f) {
|
|
// This one is blocking
|
|
handle_subproc(f);
|
|
fclose(f);
|
|
remove(fn);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (d) {
|
|
closedir(d);
|
|
}
|
|
}
|
|
|
|
// Check subprocs for input
|
|
FD_ZERO(&rfds);
|
|
FD_SET(0, &rfds);
|
|
for (i = 0; i < MAX_SUBPROCS; i += 1) {
|
|
if (subprocs[i]) {
|
|
int fd = fileno(subprocs[i]);
|
|
|
|
FD_SET(fd, &rfds);
|
|
nfds = max(nfds, fd);
|
|
}
|
|
}
|
|
|
|
do {
|
|
struct timeval timeout = {1, 0};
|
|
|
|
ret = select(nfds + 1, &rfds, NULL, NULL, &timeout);
|
|
} while ((-1 == ret) && (EINTR == errno));
|
|
if (-1 == ret) {
|
|
perror("select");
|
|
exit(EX_IOERR);
|
|
}
|
|
|
|
|
|
if (FD_ISSET(0, &rfds)) {
|
|
handle_input();
|
|
}
|
|
|
|
for (i = 0; i < MAX_SUBPROCS; i += 1) {
|
|
FILE *f = subprocs[i];
|
|
|
|
if (f && FD_ISSET(fileno(f), &rfds)) {
|
|
handle_subproc(f);
|
|
if (feof(f)) {
|
|
fclose(f);
|
|
subprocs[i] = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Heartbeat
|
|
now = time(NULL);
|
|
if (now - last_pulse > 5) {
|
|
last_pulse = now;
|
|
dispatch("PULSE");
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
usage(char *self)
|
|
{
|
|
fprintf(stderr, "Usage: %s [OPTIONS] HANDLER\n", self);
|
|
fprintf(stderr, "\n");
|
|
fprintf(stderr, "-h Display help.\n");
|
|
fprintf(stderr, "-d DIR Also dispatch messages from DIR, one per file.\n");
|
|
fprintf(stderr, "-i INTERVAL Wait at least INTERVAL microseconds between\n");
|
|
fprintf(stderr, " sending each line.\n");
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
/*
|
|
* Parse command line
|
|
*/
|
|
while (!handler) {
|
|
switch (getopt(argc, argv, "hd:i:")) {
|
|
case -1:
|
|
if (optind >= argc) {
|
|
fprintf(stderr, "error: must specify event handler.\n");
|
|
usage(argv[0]);
|
|
return EX_USAGE;
|
|
}
|
|
handler = argv[optind];
|
|
break;
|
|
case 'd':
|
|
msgdir = optarg;
|
|
break;
|
|
case 'i':
|
|
{
|
|
char *end;
|
|
long long int interval;
|
|
|
|
interval = strtoll(optarg, &end, 10);
|
|
if (*end) {
|
|
fprintf(stderr, "error: not an integer number: %s\n", optarg);
|
|
return EX_USAGE;
|
|
}
|
|
output_interval.tv_sec = interval / 1000000;
|
|
output_interval.tv_usec = interval % 1000000;
|
|
}
|
|
break;
|
|
case 'h':
|
|
usage(argv[0]);
|
|
return 0;
|
|
default:
|
|
fprintf(stderr, "error: unknown option.\n");
|
|
usage(argv[0]);
|
|
return EX_USAGE;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* tcpclient uses fds 6 and 7. If these aren't open, we keep the
|
|
* original fds 0 and 1.
|
|
*/
|
|
if (-1 != dup2(6, 0)) {
|
|
close(6);
|
|
}
|
|
if (-1 != dup2(7, 1)) {
|
|
close(7);
|
|
}
|
|
|
|
unblock(0);
|
|
setvbuf(stdout, NULL, _IOLBF, 0);
|
|
|
|
signal(SIGCHLD, sigchld);
|
|
|
|
// Let handler know we're starting up
|
|
dispatch("_INIT_");
|
|
|
|
while (running) {
|
|
loop();
|
|
}
|
|
|
|
// Let handler know we're shutting down
|
|
dispatch("_END_");
|
|
|
|
return 0;
|
|
}
|