My Eight-Year-Old Bot

January 2017 / @aaronpk

Who is Loqi?

Born in 2008

  • in a private IRC channel for my roommates
  • mostly for home automation
  • (formerly known as Nerdle)

!on kitchen

turned on the kitchen lights


paused the music

15 minutes until !off kitchen

set a timer to turn off the lights
monitored the kitchen temperature...

mmm that smells delicious

with text-to-speech in the kitchen
  • 2009: Joined Freenode
  • 2011: Joined my startup's IRC server
  • 2013: Joined Esri's IRC server
  • 2016: Learned Slack

Karma Bot

Wiki Integration

Slack Reactions




open (SH, "| ".$root_folder."dispatch.php");
print SH $nick,"\n",$username,"\n",$channel,"\n",$fullmsg,"\n";
close SH;


$nick = trim(fgets(STDIN));
$username = strtolower(trim(fgets(STDIN)));
$channel = trim(fgets(STDIN));
$msg = trim(fgets(STDIN));

if(preg_match('/gives loqi (some|an?|the) (.+)/i', $msg, $match)) { ... }
if(preg_match('/^(you|i|we) should/i', $msg, $match)) { ... }


  • Perl piped to a PHP script for actual processing
  • No restart needed to add features!
  • UDP port per channel for external scripts to make the bot talk in IRC


  • Super awkward to add new channels
  • Adding new channels requires opening a new UDP port
  • Modifying the core process to add things like /me was difficult, and required a restart

2012: ZenIRCBot

  • node.js core process connects to IRC, broadcasts incoming messages on redis
  • php and other services listen on redis for incoming messages
  • same php scripts for core bot logic
  • separate redis channel for external scripts to send messages through the bot


  • didn't namespace Redis keys, so had to run one Redis instance per IRC network
  • every plugin required its own process, so was harder to maintain

2016: TikTokBot

  • Ruby process joins an IRC channel
  • also can join Slack teams!
  • incoming messages matched against regexes, then make HTTP requests to other servers
  • response from HTTP request can trigger a message back to the channel
  • bot has an HTTP server for sending messages to any channel as well


  • none yet ;-)


  • donpdonp: i might have to just pony up the dough
  • tyler: I WANT A PONY
  • * Loqi gives tyler-iphone A PONY
  • tyler: yay!
  • * Loqi giggles
  • tyler: i never know if its the loqi AI or aaron
  • * Loqi grins profusely

Multiple Replies

$reply = Loqi::random([
  'good morning!',
  'good morning',
  'guten morgen',
  'rise and shine!'

Don't Repeat Yourself

if(!Loqi::has_said('good morning')) {
  // wait 3 hours before this can trigger again
  Loqi::said('good morning', 3*60);


Random Delays

class Loqi {
  public static function sleep($min, $max) {
    $duration = rand($min*1000, $max*1000);
    usleep($duration * 1000);


(another advantage of having the bot logic outside the IRC process!)

Don't Reply to Everything

if(rand(0,3) == 0) {


Putting it all together

if(rand(0,3) == 0) {
  if(!Loqi::has_said('good morning')) {
    // wait 3 hours before this can trigger again
    Loqi::said('good morning', 3*60);

    $reply = Loqi::random([
      'good morning!',
      'good morning',
      'guten morgen',
      'rise and shine!'


Timing is everything

  • Immediate replies look like bots
  • Random delays before replying (2-90 seconds) hide the source of the keyword triggers
  • Multiple similar replies for a trigger
  • Don't always reply to everything

the best response
is no response


  • 23:13 <kenkeiter> maxogden: ping
  • 23:13 <Loqi> pong
  • 23:13 <kenkeiter> Curse you, Loqi!!
  • 23:13 <Loqi> yeah!
  • 23:15 <Metroknow> Morning Loqi. don’t say -
  • 23:16 <Metroknow> ah dammit. outsmarted.
  • 23:16 <Metroknow> [which is a reasonably unchallenging task lately]
  • 23:17 <Metroknow> I shoulda’ said good morning
  • 23:18 <Metroknow> doh!
  • 23:19 <Metroknow> you elusive, glorious bastard.