2 * irc.c - this file is part of vomak - a very simple IRC bot
4 * Copyright 2008 Enrico Tröger <enrico(dot)troeger(at)uvena(dot)de>
5 * Copyright 2008 Dominic Hopf <dh(at)dmaphy(dot)de>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; version 2 of the License.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27 #include <sys/types.h>
29 #include <netinet/in.h>
30 #include <sys/socket.h>
50 gboolean
irc_query_names(gpointer data
)
52 irc_conn_t
*irc
= data
;
53 static gchar msg
[1024];
58 msg_len
= g_snprintf(msg
, sizeof msg
, "NAMES #%s\r\n", config
->channel
);
59 send(irc
->socket
, msg
, msg_len
, 0);
65 static void irc_send_private_message(irc_conn_t
*irc_conn
, const gchar
*target
, const gchar
*format
, ...)
67 static gchar tmp_msg
[1024];
68 static gchar msg
[1024];
76 g_vsnprintf(tmp_msg
, sizeof tmp_msg
, format
, ap
);
79 msg_len
= g_snprintf(msg
, sizeof msg
, "PRIVMSG %s :%s, %s\r\n", target
, target
, tmp_msg
);
80 send(irc_conn
->socket
, msg
, msg_len
, 0);
84 static gchar
*get_nickname(const gchar
*line
, guint len
)
86 static gchar result
[20];
89 // :eht16!n=enrico@uvena.de PRIVMSG #eht16 :df
90 for (i
= 0; i
< len
; i
++)
92 if (line
[i
] == '!' || line
[i
] == '=' || j
>= 19)
98 result
[j
++] = line
[i
];
106 static gchar
*get_private_message_sender(const gchar
*line
, guint len
)
110 // :eht16!n=enrico@uvena.de PRIVMSG GeanyTestBot :hi
111 for (i
= 0; i
< len
; i
++)
116 if (strncmp("PRIVMSG", line
+ i
+ 1, 7) == 0)
118 static gchar name
[20];
119 g_snprintf(name
, sizeof(name
), "%s :", config
->nickname
);
120 // if the receiver of the message is me, then it's a private message and we return the
121 // sender's nickname, otherwise NULL
122 if (strncmp(name
, line
+ i
+ 9, strlen(config
->nickname
) + 2) == 0)
123 return get_nickname(line
, len
);
133 static gint
get_response(const gchar
*line
, guint len
)
135 static gchar result
[4];
137 gboolean in_response
= FALSE
;
139 // :kornbluth.freenode.net 353 GeanyTestBot @ #eht16 :GeanyTestBot eht16
140 // :kornbluth.freenode.net 366 GeanyTestBot #eht16 :End of /NAMES list.
141 for (i
= 0; i
< len
; i
++)
143 // before a response code
144 if (line
[i
] != ' ' && ! in_response
)
147 // after a response code
148 if (line
[i
] == ' ' && in_response
)
155 i
++; // skip the space
157 result
[j
++] = line
[i
];
166 const gchar
*irc_get_message(const gchar
*line
, guint len
)
168 static gchar result
[1024] = { 0 };
170 gint state
= 0; // represents the current part of the whole line, separated by spaces
172 // :eht16!n=enrico@uvena.de PRIVMSG #eht16 :df foo: var
174 for (i
= 0; i
< len
; i
++)
185 else if (line
[i
] != '\r' && line
[i
] != '\n')
187 result
[j
++] = line
[i
];
196 static const gchar
*irc_get_message_with_name(const gchar
*line
, guint len
, const gchar
*name
)
201 tmp
= irc_get_message(line
, len
);
202 name_len
= strlen(name
);
204 if (strncmp(tmp
, name
, name_len
) == 0)
211 // returns a nickname argument given to cmd, it may also be "cmd for nickname", then the "for" is
213 static const gchar
*get_argument_target(const gchar
*line
, guint len
, const gchar
*cmd
)
215 static gchar result
[20];
219 // :eht16!n=enrico@uvena.de PRIVMSG #eht16 :!beer for me
220 tmp
= irc_get_message(line
, len
);
223 parts
= g_strsplit(tmp
, " ", -1);
224 if (parts
== NULL
|| parts
[0] == NULL
)
230 // if cmd doesn't match, skip it
231 if (parts
[0] != NULL
&& strcmp(parts
[0], cmd
) != 0)
237 if (parts
[1] == NULL
)
243 if (strcmp("for", parts
[1]) == 0)
245 if (parts
[2] != NULL
)
247 if (strcmp(parts
[2], "me") == 0)
250 return NULL
; // if we return NULL, the nickname of the caller is used, aka "me"
253 g_strlcpy(result
, parts
[2], sizeof(result
));
256 g_strlcpy(result
, parts
[1], sizeof(result
));
258 else if (strcmp(parts
[1], "me") == 0)
261 return NULL
; // if we return NULL, the nickname of the caller is used, aka "me"
265 g_strlcpy(result
, parts
[1], sizeof(result
));
273 // Parses the line and puts the first argument in arg1, all further arguments are concatenated
274 // in arg2. arg1 and arg2 should be freed when no longer needed.
275 // If arg1 and arg2 were set successfully, TRUE is returned, if any error occurs, FALSE is returned
276 // and arg1 and arg2 are set to NULL.
277 static gboolean
get_argument_two(const gchar
*line
, guint len
, const gchar
*cmd
,
278 gchar
**arg1
, gchar
**arg2
)
286 // :eht16!n=enrico@uvena.de PRIVMSG #eht16 :!learn keyword text to be added
287 tmp
= irc_get_message(line
, len
);
289 // -> !learn keyword text to be added
290 parts
= g_strsplit(tmp
, " ", 3);
291 if (parts
== NULL
|| parts
[0] == NULL
)
297 // if cmd doesn't match, skip it
298 if (parts
[0] != NULL
&& strcmp(parts
[0], cmd
) != 0)
304 if (parts
[1] == NULL
|| parts
[2] == NULL
)
310 *arg1
= g_strdup(parts
[1]);
311 *arg2
= g_strdup(parts
[2]);
319 static gboolean
process_line(irc_conn_t
*irc_conn
, const gchar
*line
, guint len
)
321 static gchar msg
[1024];
323 gint response
= get_response(line
, len
);
324 static gchar tmp_userlist
[1024];
326 const gchar
*content
;
328 content
= irc_get_message(line
, len
);
330 // An error occurred, try to quit cleanly and print the error
331 if (response
> 400 && response
< 503)
333 // ignore Freenode's info messages sent with error code 477
334 // (see http://freenode.net/faq.shtml#freenode-info)
335 if (response
!= 477 || strstr(line
, "[freenode-info]") == NULL
)
337 g_print("Error: %s", line
);
339 syslog(LOG_WARNING
, "received error: %d (%s)", response
, g_strstrip((gchar
*) line
));
345 // retrieve user name list
346 else if (response
== 353)
348 if (tmp_userlist
[0] == '\0')
349 g_strlcpy(tmp_userlist
, strchr(content
, ':') + 1, sizeof(tmp_userlist
));
351 g_strlcat(tmp_userlist
, strchr(content
, ':') + 1, sizeof(tmp_userlist
));
353 // retrieve end of user name list
354 else if (response
== 366)
356 if (tmp_userlist
[0] != '\0')
358 set_user_list(tmp_userlist
);
359 tmp_userlist
[0] = '\0';
362 else if (! irc_conn
->connected
)
364 // don't do anything else until we got finished connecting (to skip MOTD messages)
367 else if (strncmp("PING :", line
, 6) == 0)
369 msg_len
= g_snprintf(msg
, sizeof msg
, "PONG %s\r\n", line
+ 6); // 7 = "PING :"
370 debug("PONG: -%s-\n", msg
);
371 send(irc_conn
->socket
, msg
, msg_len
, 0);
373 // handle private message
374 else if ((priv_sender
= get_private_message_sender(line
, len
)) != NULL
)
376 // to be able to send private messages to users, you need to register your bot's
377 // nickname with Nickserv (at least on Freenode)
378 irc_send_private_message(irc_conn
, priv_sender
, "I don't like private messages!");
381 else if (strncmp(content
, "!test", 5) == 0)
383 irc_send_message(irc_conn
, get_nickname(line
, len
), "I don't like tests!");
388 * You have to register your bot with nickserv and add it to the access-list
389 * of your channel to make "!roulette kick" work.
390 * This is just tested on FreeNode. Please feel free to write patches, that
391 * will make this work on other Networks.
393 else if (strncmp(content
, "!roulette", 9) == 0)
395 gint32 rand
= g_random_int();
401 if (get_argument_two(line
, len
, "!roulette", &arg1
, &arg2
))
404 * er geht in diesen Abschnitt der Verzweigung nicht rein,
405 * wenn Kommando "!roulette kick" aufgerufen wurde.
408 * TODO: this needs a better argument-parser
412 if ( strstr(arg1
, "kick") )
414 irc_kick(irc_conn
, get_nickname(line
, len
));
421 irc_send_message(irc_conn
, NULL
, "*bang*");
422 irc_send_message(irc_conn
, get_nickname(line
, len
), "You are dead.");
428 irc_send_message(irc_conn
, NULL
, "*click*");
432 else if (strncmp(content
, "!coffee", 7) == 0)
434 const gchar
*arg
= get_argument_target(line
, len
, "!coffee");
437 arg
= get_nickname(line
, len
);
439 irc_send_message(irc_conn
, NULL
,
440 "A nice sexy waitress brings %s a big cup of coffee!", arg
);
443 else if (strncmp(content
, "!coke", 5) == 0)
445 const gchar
*arg
= get_argument_target(line
, len
, "!coke");
448 arg
= get_nickname(line
, len
);
450 irc_send_message(irc_conn
, NULL
,
451 "A nice sexy waitress brings %s a cool bottle of coke!", arg
);
454 else if (strncmp(content
, "!beer", 5) == 0)
456 const gchar
*arg
= get_argument_target(line
, len
, "!beer");
459 arg
= get_nickname(line
, len
);
461 irc_send_message(irc_conn
, NULL
,
462 "A nice sexy waitress brings %s a nice bottle of beer!", arg
);
465 else if (strncmp(content
, "!help", 5) == 0)
467 irc_send_message(irc_conn
, get_nickname(line
, len
), "please use ?? help");
470 /// TODO require op privileges for !learn
471 else if (strncmp(content
, "!learn", 6) == 0)
475 if (get_argument_two(line
, len
, "!learn", &arg1
, &arg2
))
477 gint result
= help_system_learn(arg1
, arg2
);
484 text
= g_strdup_printf("new keyword \"%s\" was added.", arg1
);
489 text
= g_strdup_printf("existing keyword \"%s\" was updated.", arg1
);
494 text
= g_strdup("an error occurred. Database not updated.");
498 irc_send_message(irc_conn
, get_nickname(line
, len
), "%s", text
);
505 irc_send_message(irc_conn
, get_nickname(line
, len
),
506 "wrong usage of !learn. Use \"?? learn\" for usage information.");
510 else if (strncmp(content
, "!alias", 6) == 0)
514 if (get_argument_two(line
, len
, "!alias", &arg1
, &arg2
))
516 // detect if arg2 has more than one word by scanning for spaces in
518 if (strchr(arg2
, ' '))
520 irc_send_message(irc_conn
, get_nickname(line
, len
),
521 "You gave me more than two arguments for !alias. I can not handle this.");
523 // check if the target actually exist and refuse if it doesn't exist
524 else if (g_hash_table_lookup(config
->data
, arg2
) == NULL
)
526 irc_send_message(irc_conn
, get_nickname(line
, len
),
527 "The given target for the alias does not exist. I will refuse your request.");
533 gchar
*alias
= g_strconcat("@", arg2
, NULL
);
535 result
= help_system_learn(arg1
, alias
);
541 text
= g_strdup_printf("new alias \"%s\" was added.", arg1
);
546 text
= g_strdup_printf("existing alias \"%s\" was updated.", arg1
);
551 text
= g_strdup("An error occurred. Database not updated.");
556 irc_send_message(irc_conn
, get_nickname(line
, len
), "%s", text
);
565 irc_send_message(irc_conn
, get_nickname(line
, len
),
566 "wrong usage of !alias. Use \"?? alias\" for usage information.");
570 else if (strncmp(content
, "?? ", 3) == 0)
572 help_system_query(content
);
574 // Hi /me, acts on "hello $nickname" and "hi $nickname", hi and hello are case-insensitive
576 else if (strstr(content
, config
->nickname
) != NULL
)
578 const gchar
*tmp_msg
= irc_get_message_with_name(line
, len
, config
->nickname
);
580 if (strncasecmp("hi", content
, 2) == 0 || strncasecmp("hello", content
, 5) == 0 ||
581 strcasecmp(", hi", tmp_msg
) == 0 || strcasecmp(", hello", tmp_msg
) == 0 ||
582 strcasecmp(": hi", tmp_msg
) == 0 || strcasecmp(": hello", tmp_msg
) == 0)
584 irc_send_message(irc_conn
, NULL
,
585 "Hi %s. My name is %s and I'm here to offer additional services to you! "
586 "Try \"?? help\" for general information and \"?? vomak\" for information about me.",
587 get_nickname(line
, len
), config
->nickname
);
589 else if (strncasecmp("thanks", content
, 6) == 0 || strncasecmp("thx", content
, 3) == 0 ||
590 strcasecmp(", thanks", tmp_msg
) == 0 || strcasecmp(", thx", tmp_msg
) == 0 ||
591 strcasecmp(": thanks", tmp_msg
) == 0 || strcasecmp(": thx", tmp_msg
) == 0)
593 irc_send_message(irc_conn
, get_nickname(line
, len
),
594 "no problem. It was a pleasure to serve you.");
602 void irc_kick(irc_conn_t
*irc_conn
, gchar
*nickname
)
604 static gchar msg
[1024];
607 irc_send_private_message(irc_conn
, "ChanServ", g_strconcat("op #", config
->channel
, NULL
));
608 msg_len
= g_snprintf(msg
, sizeof msg
, "KICK #%s %s\r\n", config
->channel
, nickname
);
609 send(irc_conn
->socket
, msg
, msg_len
, 0);
610 irc_send_private_message(irc_conn
, "ChanServ", g_strconcat("deop #", config
->channel
, NULL
));
614 static gboolean
input_cb(GIOChannel
*source
, GIOCondition cond
, gpointer data
)
619 irc_conn_t
*irc
= data
;
622 if ((buf_len
= socket_fd_gets(irc
->socket
, buf
, sizeof(buf
))) != -1)
624 ret
= process_line(irc
, buf
, buf_len
);
628 irc_conn_t
*irc
= data
;
630 if (cond
& (G_IO_IN
| G_IO_PRI
))
638 rv
= g_io_channel_read_line(source
, &buf
, &buf_len
, NULL
, &err
);
642 buf
[buf_len
] = '\0'; // skip trailing \r\n
644 process_line(irc
, buf
, buf_len
);
649 debug("%s: error: %s", __func__
, err
->message
);
654 while (rv
== G_IO_STATUS_NORMAL
|| rv
== G_IO_STATUS_AGAIN
);
655 debug("%s: status %d\n", __func__
, rv
);
662 void irc_send_message(irc_conn_t
*irc_conn
, const gchar
*target
, const gchar
*format
, ...)
664 static gchar tmp_msg
[1024];
665 static gchar msg
[1024];
669 va_start(ap
, format
);
670 g_vsnprintf(tmp_msg
, sizeof tmp_msg
, format
, ap
);
674 msg_len
= g_snprintf(msg
, sizeof msg
, "PRIVMSG #%s :%s, %s\r\n", config
->channel
, target
, tmp_msg
);
676 msg_len
= g_snprintf(msg
, sizeof msg
, "PRIVMSG #%s :%s\r\n", config
->channel
, tmp_msg
);
678 send(irc_conn
->socket
, msg
, msg_len
, 0);
682 void irc_goodbye(irc_conn_t
*irc
)
687 len
= g_strlcpy(msg
, "QUIT :Good bye. It was a pleasure to serve you\r\n", sizeof msg
);
688 send(irc
->socket
, msg
, len
, 0);
692 gint
irc_finalize(irc_conn_t
*irc_conn
)
694 if (irc_conn
->socket
< 0)
697 if (irc_conn
->lock_tag
> 0)
698 g_source_remove(irc_conn
->lock_tag
);
700 if (irc_conn
->read_ioc
)
702 g_io_channel_shutdown(irc_conn
->read_ioc
, TRUE
, NULL
);
703 g_io_channel_unref(irc_conn
->read_ioc
);
704 irc_conn
->read_ioc
= NULL
;
706 socket_fd_close(irc_conn
->socket
);
707 irc_conn
->socket
= -1;
713 void irc_connect(irc_conn_t
*irc_conn
)
716 struct sockaddr_in their_addr
;
722 // Connect the socket to the server
723 if ((he
= gethostbyname(config
->server
)) == NULL
)
725 perror("gethostbyname");
729 if ((irc_conn
->socket
= socket(PF_INET
, SOCK_STREAM
, 0)) == -1)
735 their_addr
.sin_family
= PF_INET
;
736 their_addr
.sin_port
= htons(6667);
737 their_addr
.sin_addr
= *((struct in_addr
*)he
->h_addr_list
[0]);
738 memset(&(their_addr
.sin_zero
), '\0', 8);
740 if (connect(irc_conn
->socket
, (struct sockaddr
*)&their_addr
, sizeof(struct sockaddr
)) == -1)
746 msg_len
= g_snprintf(msg
, sizeof(msg
), "USER %s %s %s :%s\r\nNICK %s\r\n",
747 config
->username
, config
->servername
, config
->servername
, config
->realname
, config
->nickname
);
748 if (send(irc_conn
->socket
, msg
, msg_len
, 0) == -1)
753 if (NZV(config
->nickserv_password
))
755 msg_len
= g_snprintf(msg
, sizeof(msg
), "PRIVMSG nickserv :identify %s\r\n", config
->nickserv_password
);
756 if (send(irc_conn
->socket
, msg
, msg_len
, 0) == -1)
758 perror("send NICKSERV");
762 g_snprintf(msg
, sizeof msg
, "JOIN #%s\r\n ", config
->channel
);
763 if (send(irc_conn
->socket
, msg
, strlen(msg
), 0) == -1)
768 // input callback, attached to the main loop
769 irc_conn
->read_ioc
= g_io_channel_unix_new(irc_conn
->socket
);
770 //~ g_io_channel_set_flags(irc_conn.read_ioc, G_IO_FLAG_NONBLOCK, NULL);
771 g_io_channel_set_encoding(irc_conn
->read_ioc
, NULL
, NULL
);
772 irc_conn
->lock_tag
= g_io_add_watch(irc_conn
->read_ioc
, G_IO_IN
|G_IO_PRI
|G_IO_ERR
, input_cb
, irc_conn
);