aboutsummaryrefslogtreecommitdiff
irctk -- an IRC toolkit
Copyright (C) 2010-2015 by Antoine Amarilli

== 0. License ==

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, version 3.

This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
this program (see file "COPYING").  If not, see <http://www.gnu.org/licenses/>.

== 1. Description ==

irctk is a general-purpose IRC toolkit. It connects to an IRC server specified
on the command line, joins channels specified on the command line, posts what it
receives from stdin to the channels and outputs to stdout what it hears on the
channels. This makes it possible to write simple IRC bots and scripts very
quickly, either in the shell or in your favourite programming language.

== 2. Installation ==

You will need the libircclient library (version >= 1.8 with SSL support) to
compile and run irctk, as well as libssl-dev. On Debian Jessie and later,
sufficiently recent versions can be installed with:

  sudo apt-get install libircclient1 libircclient-dev libssl-dev

You can then compile irctk by issuing "make". Install irctk by yourself in a
location of your PATH if you want to use it as "irctk", otherwise replace
"irctk" by "./irctk" in the next examples.

The rest of this section presents how to compile libircclient by yourself if
necessary, and how to install irctk without requiring root privileges.

=== 2.1. Compiling libircclient ===

If you need to install libircclient manually, get libircclient-1.8 from
<http://sourceforge.net/projects/libircclient/>, compile it and install it (this
requires the package libssl-dev on Debian, and of course make, a C compiler,
etc., which you can get on Debian systems by installing e.g. the package
build-essential):

  ./configure --enable-openssl  --enable-shared
  make
  sudo make install

=== 2.2. Installing without root ===

If you cannot install libircclient system-wide, compile it as previously
explained, without the "sudo" command. Then, issue (in the libircclient folder):

  ln -s src/libircclient.so src/libircclient.so.1

Now go back to the irctk folder, add at the end of the CFLAGS line the
following, adjusting for the location of the libircclient folder:

  -I/where/you/put/libircclient-1.8/include

Compile with "make", and now run irctk with the following invocation:

  LD_LIBRARY_PATH="/where/you/put/libircclient-1.8/src/:$LD_LIBRARY_PATH" ./irctk

== 3. How to use ==

=== 3.1. Connecting to a server ===

As a simple interactive use of irctk, here is how you connect to the IRC server
at example.com and join channel #test:

  $ irctk example.com '#test'

Messages on the channel will get output on your terminal, and you can type
messages that you want so say to the channel. Press ^D to close stdin and
terminate irctk. Note that the use of quotes around "#test" is to prevent your
shell from interpreting '#' (irctk won't see them).

More elaborate options are supported. Here is how to connect to a
password-protected channel on a password-protected server on a non-standard
port, specifying a custom nickname, username and real name.

  $ irctk -U jh -R "J. Hacker" nick:srvpass@example.com:3724 '#test:chanpass'

To connect to a server with SSL support, run:

  $ irctk --ssl example.com

To additionally disable SSL certificate checking, and allow self-signed and
invalid certificates (at the risk of falling in a man-in-the-middle attack),
run:

  $ irctk --ssl --no-check-certificate example.com

Connection will fail if you specify --ssl but the server does not support SSL,
or vice versa.

=== 3.2. Using irctk's stdin ===

irctk is meant to be used non-interactively. For instance, you can say the
contents of a file on a channel by giving it as standard input:

  $ irctk flooder@example.com '#flood' <file

Of course, it is more interesting to pipe to irctk something which will produce
more and more lines as events occur. For instance, here is how to follow your
server logs on IRC:

  $ ssh server tail -f logfile.log | irctk logger@example.com '#dashboard'

If you receive mail to a mbox file, here is how you could use irctk to
get a private message to notify you about the subjects of incoming emails.

  $ tail -f ~/mbox | grep --line-buffered '^Subject:' |
      irctk alert@example.com mynick

Note the use of --line-buffered to make sure that the messages do not get
buffered. Here is how to follow the RSS feed of FML and post the stories to a
channel as they appear in the feed:

  $ rsstail -u 'http://feeds2.feedburner.com/fmylife' -NdzH -n 1 -i 300 |
      grep --line-buffered '^ Today' |
      irctk fmlbot@example.com '#fml'

=== 3.3. Using irctk's stdout ===

You can log what is happening on a channel by setting stdout to be a file:

  $ irctk logger@example.com '#chan' >file

You can add timestamps:

  $ irctk logger@example.com '#chan' |
      awk '{ print strftime("%s"), $0; fflush() }' > file

Caution, if you want to run irctk in the background to do something like this,
you need to prevent it from reading stdin (to avoid it being suspended) without
closing stdin (otherwise irctk will terminate). Here is how:

  $ tail -f /dev/null | irctk logger@example.com '#chan' >file &

Another example: play a sound whenever your nick is mentioned (but not when you
speak):

  $ irctk example.com '#chan' |
      grep --line-buffered '[^<]yournick' | while read l; do
        aplay alert.wav;
      done

irctk has specific features to detect when someone addresses it. Say you want to
log tasks to do by addressing a bot on IRC:

  $ irctk -f todobot@example.com '#chan' >>~/todo

To append lines to ~/todo, you can either address todobot on #chan through
messages like "todobot: buy some milk", or you can send a private message to
todobot (using irssi, "/msg todobot write a poem to alice"). Note that the lines
logged in ~/todo will look like "[#chan] <mynick> todobot: buy some milk"; if
you want to get rid of the cruft, you can use:

  $ irctk -F todobot@example.com '#chan' >>~/todo

which will only log "buy some milk" (and implies -f).

To combine the use of stdin and stdout, this invocation pipes two irctk calls
together to relay messages from source.com to destination.com (but not the
reverse):

  $ irctk listener@source.com '#chan1' |
     irctk repeater@destination.com '#chan2'

=== 3.4. Writing interactive bots ===

We will now look at interactive examples where you interface irctk's stdout to
some script or program which will do something intelligent and give something to
say to irctk in return. To do so, we will need a named FIFO:

  $ mkfifo fifo

As an extremely simple interactive program, consider the following:

  $ cat fifo | irctk pongbot@example.com '#chan' |
      awk '/[^<]ping/ { print "pong"; fflush() }' > fifo

The awk invocation outputs "pong" whenever it sees a line containing "ping"
(excluding things such as "<ping" to avoid the issue of people with a nick
starting with "ping"). This means that pongbot will say "pong" on #chan whenever
someone says something containing "ping". Note the use of fflush(), once again
to avoid buffering. The named FIFO is used to connect irctk's stdout to awk's
stdin and awk's stdout to irctk's stdin. Note that the cat invocation is
required and "<fifo" will not work.

Of course, you can use your favorite programming language instead of awk. If you
want to write an IRC bot in an esoteric language with no IRC library (or maybe
even no networking support), all you need to do is write some code which reads
incoming lines on stdin, posts outgoing lines on stdout, and *does not buffer*.
You can then lift your program to IRC very simply:

  $ cat fifo | irctk example.com '#chat' | program > fifo

=== 3.5. Input and output format ===

The output format of irctk is of the following form (unless you use -F):

  [#chan] <nick> message

By default, server events (joins, kicks, renames, etc.) are not output. If you
want them, you can either get them in a human-readable form with the -m flag:

  [#bar] -!- foo has joined #bar

Alternatively, you can get them in a barebones form with -c:

  [#bar] <foo> /join #bar

Your own messages will not be included unless you specify --own. If you want to
see nicknames like <nick!~username@localhost>, use --with-host. You can also use
the -f and -F options presented above to only keep lines addressed to you and to
remove everything but the message (-F implies -f).

The input format is of the following form:

  [channel] message

The channel can be either of the form "#chan" (a regular channel) or of the form
"user" (the channel of private messages exchanged with user). You can specify
multiple channel names separated by commas (but see the section "Pipelining"
below).

Because specifying the chan each time can be tedious, irctk can try to guess it.
If you do not specify a destination and just give a message, irctk will say it
to the last active channel by default (i.e., the last channel where something
was heard), which is often a reasonable choice if you are replying to someone.
There are other possible options: see the section "Implied destinations" below.

irctk will always try to join a channel before saying something to this channel.
This means that it can join entirely new channels in this fashion. To disable
this behavior and prevent irctk from joining any channels except the ones given
at startup, use --no-join (can be useful if irctk's stdin is
attacker-controlled). Note that this only affects the behavior of irctk on
regular channels: even with --no-join, irctk will be able to send private
messages to anyone, and will try to send messages to unknown channels (just
without trying to join them first).

irctk will interpret some commands starting with '/' in a fashion similar to
irssi. To inhibit this (can be useful if irctk's stdin is attacker-controlled),
use --no-command-to-event.

When irctk is provided attacker-controlled input, the right way to escape is to
prepend '/say' or '/ ' before every line provided to irctk (be careful if the
attacker may provide newlines).

The supported commands are:

  /nick NICKNAME (change nick)
  /mode MODE (set channel mode)
  /part [CHAN] (part from a channel)
  /join [CHAN] (join a channel)
  /topic TOPIC (set channel topic)
  /quit REASON (quit)
  /invite USER [CHAN] (invite a user to a channel)
  /kick USER [REASON] (kick user from current inferred destination)
  /me MSG (/me message)
  /notice MSG (say as a notice)
  /oper USER [PASS] (obtain operator privileges)
  /quote COMMAND (send raw command to IRC server)
  /say MSG (escape)
  / MSG (escape)
  /notice MSG (like /say but use NOTICE)

Optional channel names "[CHAN]" in the above list default to the current
inferred destination (i.e., the last active channel by default).

As an additional convenience, irctk can be made to address the last person who
addressed it, with the -r option. In conjunction with the default destination
channel inference, this means that, using -fr, whenever you ask irctk to say
"message", it will say that to the last person who addressed it, on the channel
where it was addressed. This is very convenient to write bots.

=== 3.6. Complete examples ===

This bot queries user names using finger and returns a status line for them (or
an error if they do not exist):

cat fifo | irctk -Fr fingerbot@example.com '#chat' |
  while read; do
    finger -s -- "$REPLY" 2>&1 | tail -1
  done >fifo

The following bot can be used to roll dice: say something like "dmbot: 3d42" and
it will reply with the result. Note that this example is bash-specific. Thanks
to p4bl0 <http://pablo.rauzy.name/> for writing it.

cat fifo | irctk -Fr dmbot@example.com '#chat' |
  while read line; do
    if grep -E '^[0-9]{1,2}d[0-9]{1,3}$' <<<"$line" &>/dev/null; then
      D=(${line/d/ })
      for ((i = 0; i < ${D[0]}; i++)); do
        echo -n $((RANDOM % ${D[1]} + 1))" "
      done
      echo
    else
      echo "format error: must be NdM with N<100 and M<1000"
    fi
  done >fifo

This bot queries on wikipedia whatever is said to it, using the DNS-based
wikipedia query system as an ugly way to get the beginning of pages.

  $ cat fifo | irctk -Fr wikibot@example.com '#chat' |
      while read line; do
        Q=$(echo "$line" | tr ' ' '_' | tr -dc 'a-zA-Z_()');
        dig +short txt $Q.wp.dg.cx; echo;
      done >fifo

This is a way to play adventure on IRC. (Beware, people can overwrite files when
saving their game, so run as a suitably unpriviledged user.) The "while true"
loop is to restart the game whenever it exits. The socat invocation is used to
disable buffering. To play, say "DM: command".

  $ cat fifo | irctk -F DM@example.com '#adventure' |
      while true; do
        socat EXEC:adventure,pty,ctty,echo=0 STDIO;
      done >fifo

Two-way gateway: gateway posts on #chan1 on server1.com whatever is said to it
on #chan2 on server2.com, and vice-versa:

  $ cat fifo | irctk -F0 gateway@server1.com '#chan1' |
      irctk -F0 gateway@server2.com '#chan2' | tee fifo

Run shell commands from your IRC client (just by saying them in #tty, no need to
address the bot). BEWARE, this means that whoever is named "admin" on the IRC
server can run arbitrary commands on your machine, so you really shouldn't do
this.

  $ cat fifo | irctk localhost '#tty' |
      grep --line-buffered '^\[#tty\] <admin>' |
      sed -u 's/^[^>]*> *//' | bash >fifo 2>&1

Whatever admin says on #tty will get run in a bash shell and the result will be
returned to the channel. Note that you can of course run irctk in this shell
(irception!), but beware of feedback loops if you attempt to join the same
channel!

=== 3.7. Implied destinations ===

You can always specify the channel to which you speak by using a "[#channel]"
prefix. You can specify multiple channels for the same message using commas (but
see "Pipelining"). If you do not specify a channel, then irctk will choose one
itself. Note that you can start your message with "[]" if your message starts
with a '[' but you want irctk to infer the channel.

Several possible ways to choose are available, only one of them can be provided
on the command line. Here are those options, sorted by ascending complexity. A
discussion of other useful options follows.

  * --default-always-first

Messages with no destination will be sent to the first channel specified on the
command line invocation of irctk (or to irctk's private channel if none were
specified, which is not very useful).

  * --default-all

Messages with no destination will be sent to all channels specified on the
command line invocation of irctk. (They will *not* be sent to other channels
that might have been joined by irctk later.)

  * --default-last-posted

Messages with no destination will be sent to the last channel to which a message
was sent. This is useful if you are writing to stdin manually and want to
specify the channel only when it changes. Note that you can use -P to display
the current default destination on stderr; if you send irctk's stdout elsewhere
to avoid clobbering your terminal, this makes irctk (almost) look like irssi's
prompt. (For instance, you can send stdout to a FIFO and display it in another
window (or use GNU screen) to get a poor man's IRC client.)

  * --default-last-active (default)

Messages with no destination will be sent to the last active channel, that is,
the last channel on which something took place. This is reasonable if you want
to react instantaneously to something which just happened. Note that because
irctk reads stdin as greedily as possible, the last active channel should be the
last active at the moment when you *write* your message to irctk's stdin, not at
the moment when irctk will *say* it (the two differ if irctk has a long pipe of
things to say). irctk's behavior is usually what you expect here.

The --default-last-active option is perfect if you want to reply to messages by
users and if your replies are instantaneous. If your replies take time and other
requests may arrive during that time, irctk will not be able to route the
answers on its own: consider writing your own logic to route messages according
to your needs (and always specify the destination explicitly).

=== 3.8. Tracking ===

Because of the delay between messages which irctk observes to avoid getting
kicked by a pissed server, messages can be sent to the server a long time after
irctk received them on stdin. This means that if you addressed someone doing
something like "nick: message" or "[nick] message" or using -r, then that person
might have changed nick in the meantime and the message may not get routed
correctly.

As a countermeasure, you can specify --track-renames so that messages addressed
to a user in one of the above fashions get sent to the user's current nick.
(They will get sent to their last known nick if they part or quit.) There is
also a --unique-names options with which irctk will maintain unique names for
users (based on the first seen nick for a user), expose them on stdout, and
expect them on stdin. This is useful if you want to write a bot which stores
e.g. a score for each user and if you want users to keep their score even if
they change nick. These tracking modes are not enabled by default.

Note: because a rename may be seen too late, this option is not guaranteed to
work, and some messages may get mistakenly addressed to an older nick.

=== 3.9. Pipelining ===

irctk has a built-in rate limitation (configurable with -i) which it tries to
apply independently on each channel. This means that, to ensure the fastest
possible delivery, messages to channels with an empty queue will be sent
*before* messages to channels with a busy queue. no matter the order on which
they were provided on standard input. However, within a given channel, the order
relation on messages will match their order on standard input.

If you specify multiple destination channels like "[#a,#b,#c]", however, the
resulting message will be said on all the channels simultaneously (and will
therefore wait for the buffers of all relevant channels to be empty). If you do
not want this synchronization, you should say the message several times,
addressed to each individual channel.

Beware of the fact that the IRC server may limit irctk's rate in a fashion which
irctk will not be able to control, so any slowdowns you see may not be irctk's
fault. Use -o to see when irctk is sending your messages, to see who is slowing
things down.

== 4. Test suite ==

You can run the test suite with ./tests_sequential.sh. This requires a working
IRC server on localhost:6667. I use ircd-hybrid from Debian testing, configured
with throttle_time = 0 and anti_spam_exit_message_time = 0. This also requires
valgrind (which is packaged for Debian).

If you have GNU parallel and if your IRC server isn't afraid of many connections
from a single IP, you can run the tests in parallel: ./tests_parallel.sh. This
isn't guaranteed to work.

== 5. Caveats, limitations and FAQ ==

irctk has not been thoroughly audited for bugs or security vulnerabilities. Be
cautious if you intend to connect it to an IRC server with untrusted users.

IRC servers will usually throttle clients. If want to set up chatty bots, you
will need to have control over the IRC server and configure it adequately (for
ircd-hybrid, look at "can_flood" and also at "default_floodcount").

irctk will exit whenever it has sent all messages on stdin to the server. If the
server throttles it, then it might exit before all messages have been
delivered, and some may get lost. Use -i and -I, or sleep for a few seconds
before closing stdin.

You need to use tail -f /dev/null as input if you want to background irctk
without having it suspend or exit (see above).

irctk may say greedily the first things it sees on stdin while things to say in
parallel might be available later.

irctk can have trouble with buffering. When writing pipelines involving irctk,
be sure to disable all buffering (sed -u, awk's fflush, python -u, stdbuf in
some cases, etc.).

If you get a "LIBIRC_ERR_SSL_NOT_SUPPORTED not declared" error when compiling,
it means you are not compiling against the right version of libircclient (see
section 2).

== 6. Related projects ==

  * ii <http://tools.suckless.org/ii/>

ii is filesystem and FIFO-based but irctk is entirely FIFO-based. ii's control
FIFO is irctk's stdin, but ii's output files are replaced by irctk's stdout.
irctk does not write to disk or read from disk. irctk also includes features
which make it easy to write bots in shell script one-liners.

  * sic <http://tools.suckless.org/sic>

sic is pretty similar to irctk, except irctk abstracts more things from the
underlying IRC protocol and has more features (e.g., SSL support and the various
options). Conversely, sic is <= 250 LOC without dependency on an external
library.

  * IrcTK <https://github.com/maxcountryman/irctk>

irctk has nothing to do with this except the similar name.