Socket Bot Tutorial

Contributed by myndzi
Tutorial - mIRC 6.0 Socket Bot I find myself frequently going over the basics of creating an IRC socket bot with people. I have developed a framework which I use whenever I make socket IRC connections, and I decided it was worth sharing ;)

So, I threw together the following script, updated a little for mIRC 6.0's signal command and events, with a basic set of features and identifiers to make things easy. I will also be submitting this code as a snippit, without all my blabbering interspersed.

I am assuming that the reader has basic knowledge of mIRC's features including sockets, identifiers, aliases, and misc. other things I used a little of (windows, events, groups, local aliases, local variables..)

I will start simply by presenting you with the code, and afterwards I will break it down alias-by-alias, event-by-event.


;--Begin--

;Socket Bot Sample 1.0 - myndzi

;sb_open host [port]
alias sb_open {
  if ($sock(sb)) return
  if ($window(@sb_debug)) titlebar @sb_debug - Connecting
  .enable #sb_connecting
  sockopen sb $$1 $iif($2,$2,6667)
}

;sb_close
alias sb_close sb_cleanup

;sb_quit [message]
alias sb_quit sb_out QUIT $iif($0,: $+ $1-)

;sb_out 
alias sb_out {
  if ($sock(sb) == $null) return
  if ($window(@sb_debug)) echo 10 -ti2 @sb_debug >> $1-
  sockwrite -n sb $1-
}

;sb_debug
alias sb_debug {
  window -e @sb_debug
  if ($_online) titlebar @sb_debug - Nick: $_me
  else titlebar @sb_debug - Disconnected
}

alias -l sb_cleanup {
  if ($window(@sb_debug)) titlebar @sb_debug - Disconnected
  sockclose sb
  .disable #sb_connecting
  .timer 1 0 unset %sb_me
  .signal -n sbeDISCONNECT
}

alias -l sb_nick return SockBot
alias -l sb_nick2 return SB[ $+ $r(1000,9999) $+ ]
alias -l sb_user return bot
alias -l sb_realname return Socket Bot Sample 1.0 - myndzi

alias _getnick return $gettok($_strip:($1),1,33)
alias _getuser return $gettok($gettok($1,2,33),1,64)
alias _gethost return $gettok($1,2,64)
alias _strip: return $iif(:* iswm $1,$right($1,-1),$1)
alias _me if ($isid) return %sb_me | set %sb_me $$1 | if ($window(@sb_debug)) titlebar @sb_debug - Nick: %sb_me
alias _online return $iif($sock(sb),$true,$false)

on *:input:@sb_debug:{
  if ($left($1,1) == $readini($mircini,text,commandchar)) && (!$ctrlenter) return
  sb_out $1-
  haltdef
}

on *:sockopen:sb:{
  if ($sockerr) return $sb_cleanup
  sb_out NICK $sb_nick
  sb_out USER $sb_user * * :  $+ $sb_realname
}
on *:sockread:sb:{
  if ($sockerr) return $sb_cleanup
  var %t
  while ($sock(sb).rq) {
    sockread %t | tokenize 32 %t
    if ($sockbr == 0) return
    if ($window(@sb_debug)) echo 11 -ti2 @sb_debug << $1-
    if ($1 == PING) sb_out PONG $2-
    else .signal -n sbe_ $+ $2 $1-
  }
}
on *:sockwrite:sb:if ($sockerr) return $sb_cleanup
on *:sockclose:sb:return $sb_cleanup

on *:signal:sbe_NICK:if ($_getnick($1) == $_me) _me $_strip:($3)
on *:signal:sbe_PRIVMSG:{
  if (:ACTION * iswm $4-) .signal -n sbeACTION $1 ACTION $3 $left($5-,-1)
  elseif (:* iswm $4-) .signal -n sbeCTCP $1 CTCP $3 $mid($_strip:($4-),2,-1)
  else .signal -n sbeTEXT $1 TEXT $3 $_strip:($4-)
}
on *:signal:sbe_NOTICE:{
  if (:* iswm $4-) .signal -n sbeCTCPREPLY $1 CTCPREPLY $3 $mid($_strip:($4-),2,-1)
  elseif (. isin $_getnick($1)) .signal -n sbeSNOTICE $1 SNOTICE $3 $_strip:($4-)
  else .signal -n sbeNOTICE $1 NOTICE $3 $_strip:($4-)
}

#sb_connecting off
on *:signal:sbe_433:sb_out NICK $sb_nick2
on *:signal:sbe_422:sb_connected $3
on *:signal:sbe_376:sb_connected $3

alias -l sb_connected {
  _me $1
  if ($window(@sb_debug)) titlebar @sb_debug - Nick: $_me
  .disable #sb_connecting
  .signal -n sbeCONNECT
}
#sb_connecting end


;--End--


OK! Wasn't that fun? Let's get started!

First off, we have the public aliases -- these are aliases that are meant to be used by scripts or from an editbox somewhere.

;This alias creates the bot connection.
alias sb_open {

  ;if it already exists, don't go any further
  if ($sock(sb)) return

  ;if the debug window is open, change the titlebar to reflect what we're doing
  if ($window(@sb_debug)) titlebar @sb_debug - Connecting

  ;enable the group (at the bottom) that will grab our nick and trigger the CONNECT event
  .enable #sb_connecting

  ;actually open the socket
  sockopen sb $$1 $iif($2,$2,6667)
}


;This alias closes the socket and cleans everything up (calls sb_cleanup)
alias sb_close sb_cleanup


;This alias sends a QUIT message; do not close socket (wait for server to do so)
alias sb_quit sb_out QUIT $iif($0,: $+ $1-)


;This alias sends data to the server
alias sb_out {

  ;if the socket is not open, go no further
  if ($sock(sb) == $null) return

  ;if the debug window is open, show it what's happening
  if ($window(@sb_debug)) echo 10 -ti2 @sb_debug >> $1-

  ;send the data
  sockwrite -n sb $1-
}


;This alias opens the debug window
alias sb_debug {

  ;create window with editbox
  window -e @sb_debug

  ;if we're connected, set titlebar to show which nick
  if ($_online) titlebar @sb_debug - Nick: $_me

  ;otherwise set it to "Disconnected"
  else titlebar @sb_debug - Disconnected
}

OK, those were all pretty basic. In fact, this whole script is pretty basic. On to the next part!


Local aliases. These perform functions for the rest of the script that the "outside" doesn't need access to.

;This alias cleans up everything when the socket closes/is closed
alias -l sb_cleanup {

  ;if the debug window is open, set the titlebar to reflect that we are no longer connected
  if ($window(@sb_debug)) titlebar @sb_debug - Disconnected

  ;close the socket
  sockclose sb

  ;see near the end of this document for the #sb_connecting group info
  .disable #sb_connecting

  ;unset %sb_me (socket bot's nick) after this alias concludes
  .timer 1 0 unset %sb_me

  ;send out a signal that we have become disconnected
  ;see below for more info, note the lack of an underscore
  .signal -n sbeDISCONNECT
}

;These four aliases return information used on connect to the server -- change at will
alias -l sb_nick return SockBot
alias -l sb_nick2 return SB[ $+ $r(1000,9999) $+ ]
alias -l sb_user return bot
alias -l sb_realname return Socket Bot Sample 1.0 - myndzi

The only thing out of those that might need explaining is why I set a timer to unset %sb_me -- this is because I want to unset the variable, but I want $_me to work even inside the "sbeDISCONNECT" signal events; if one of those events halts everything for whatever reason, the variable will still get unset this way.


Now come the public identifiers, which I have prefixed with a _ because some of them might conflict with existing identifiers in some cases. The first three use string manipulations to retreive separate parts of a string such as ":[email protected]" via tokens.

alias _getnick return $gettok($_strip:($1),1,33)
alias _getuser return $gettok($gettok($1,2,33),1,64)
alias _gethost return $gettok($1,2,64)

;Ascii character 33 is the "!" and 64 is the "@".

;The next identifier strips a preceding ":" character if it is there:
alias _strip: return $iif(:* iswm $1,$right($1,-1),$1)

;This identifier returns the bot's nick or sets it to a new nick.
;I do not recommend calling this one yourself as a command;
;  it should only be called from the sbe_NICK signal included.
;This identifier also updates the @sb_debug window if it is open.
alias _me if ($isid) return %sb_me | set %sb_me $$1 | if ($window(@sb_debug)) titlebar @sb_debug - Nick: %sb_me

;This identifier returns $true if the socket is connected, $false otherwise.
alias _online return $iif($sock(sb),$true,$false)


This event is only applicable to the @sb_debug window. It sends text typed in this window to the server via sb_out, thus echoing to itself as well. If the text starts with mIRC's command character, it does nothing, unless ctrl+enter was pressed.

on *:input:@sb_debug:{
  if ($left($1,1) == $readini($mircini,text,commandchar)) && (!$ctrlenter) return
  sb_out $1-
  haltdef
}


Now comes the "main" portion of all of this. This is the basics of any IRC socket I've used for whatever purpose. The sockopen, sockwrite, and sockclose events do relatively little, it's the sockread event that does the work. I'll get the first three out of the way, and go into more detail on the sockread event itself.

on *:sockopen:sb:{

  ;clean stuff up and close if there's an error
  if ($sockerr) return $sb_cleanup

  ;send login info to IRC server
  sb_out NICK $sb_nick
  sb_out USER $sb_user * * :  $+ $sb_realname
}

;clean stuff up if there's an error
on *:sockwrite:sb:if ($sockerr) return $sb_cleanup

;clean stuff up regardless
on *:sockclose:sb:return $sb_cleanup


;The fun part!!
on *:sockread:sb:{

  ;clean stuff up if there's an error
  if ($sockerr) return $sb_cleanup

  ;define %t as a temporary variable -- it will be unset when this event concludes
  var %t

  ;execute this loop while there is data to be read from the socket
  while ($sock(sb).rq) {

    ;read a line of data into the temp var, then "tokenize" it with character 32 (space)
    ;tokenizing makes it easy to access the text in space-separated words.
    sockread %t | tokenize 32 %t

    ;if nothing was read, return. this will ONLY happen if for some reason, say,
    ;part of a line was sent in one packet, but it got ended before a CRLF combination.
    if ($sockbr == 0) return

    ;if the debug window is open it, inform it of the read data
    if ($window(@sb_debug)) echo 11 -ti2 @sb_debug << $1-

    ;if the incoming data was a PING from the server, respond immediately!
    if ($1 == PING) sb_out PONG $2-

    ;otherwise, we send out a signal command based on the second token.
    ;PING is the only command we get from the server where the second token
    ;does NOT tell us what the line is all about.
    else .signal -n sbe_ $+ $2 $1-
  }
}

OK! In the "olden days" of pre-mIRC 6.0 I used a different method than signal. Here's how it works, for those who are interested:

if ($isalias(sbe_ $+ $2)) sbe_ $+ $2 $1-

All it does is call an alias such as "sbe_JOIN" if said alias exists, using the entire read line from the server as parameters.

"These days" with the spiffy signal command, I have a better way of doing it. It is advantageous to us to use signal because we can now react to one "event" with multiple scripts, just like real events whereas with the $isalias method you could only have one (without complicating things further).

Note that I "silenced" the signal command with the . prefix -- signal is "noisy" by default, echoing to your status window every time it is executed. You probably don't want this considering it will be called every time your bot reads a line of data from the server. I also used the "-n" switch, which means "trigger NOW" -- this will make the events happen in order, as they are read, as opposed to all triggering after the sockread event is done.

Lastly, I prefixed the signal name with "sbe_" -- "socket bot event" -- so as not to accidentally trigger any signals not meant to work with this bot. As it will be called with everything from numerics to events such as JOIN, NICK, etc. this is probably a good idea.

Later, you will see me trigger some signals named like "sbeCONNECT" and "sbeTEXT" -- these are NOT a mistake. I left the underscore out because these signals will not be triggered directly as a result of text being read from the socket -- they are triggered as sort-of a "second layer" of events. There is a sbeCONNECT, sbeDISCONNECT, sbeTEXT, sbeNOTICE, sbeSNOTICE, sbeCTCP, sbeCTCPREPLY, and sbeACTION. You can add more later if you want, but these I decided to include for your convenience.


Here are the included signals. I recommend you leave these exactly as they are.

;This signal triggers when you receive a ":nick!ident@host NICK :newnick" message.
;It checks if the bot is the one changing nicks, and if so, updates the bot's "me" variable.
;The _me alias will also update the debug window's titlebar if it is open.
on *:signal:sbe_NICK:if ($_getnick($1) == $_me) _me $_strip:($3)

;This signal triggers when you receive a ":nick!ident@host PRIVMSG target :text" message.
;It breaks this down into three possibilities - ACTION, CTCP, or TEXT, and triggers the
;  appropriate signals for each. Note that these "second layer" signals do not have the
;  underscore in them -- this is to avoid conflict with possibly valid text received from
;  the server with the same name.
on *:signal:sbe_PRIVMSG:{

  ;the boxes are $chr(1). $left($5-,-1) is all the text after the word "ACTION", without
  ;the trailing $chr(1) character.
  if (:ACTION * iswm $4-) .signal -n sbeACTION $1 ACTION $3 $left($5-,-1)

  ;this is just a "standard" CTCP. it triggers the sbeCTCP signal including the CTCP type
  ;where "ACTION" left it out. it also strips the $chr(1) characters off, as well as the
  ;preceding : character.
  elseif (:* iswm $4-) .signal -n sbeCTCP $1 CTCP $3 $mid($_strip:($4-),2,-1)

  ;this sends out a signal for "regular" text. it also strips the preceding : character.
  else .signal -n sbeTEXT $1 TEXT $3 $_strip:($4-)
}

;This signal triggers when you receive a ":nick!ident@host NOTICE target :text" message.
;It breaks this down into two possibilities - CTCPREPLY or NOTICE and triggers the
;  appropriate signals for each. Note that these "second layer" signals do not have the
;  underscore in them -- this is to avoid conflict with possibly valid text received from
;  the server with the same name.
on *:signal:sbe_NOTICE:{

  ;the boxes are $chr(1). this triggers the sbeCTCPREPLY signal and strips $chr(1) and :
  if (:* iswm $4-) .signal -n sbeCTCPREPLY $1 CTCPREPLY $3 $mid($_strip:($4-),2,-1)

  ;triggers the sbeSNOTICE signal for a server notice. assumes that if there is a period
  ;in the nickname it is a notice from the server.
  elseif (. isin $_getnick($1)) .signal -n sbeSNOTICE $1 SNOTICE $3 $_strip:($4-)

  ;triggers a signal for a "regular" notice
  else .signal -n sbeNOTICE $1 NOTICE $3 $_strip:($4-)
}

OK, I hope that all made sense! Basically I created new signal events to break up PRIVMSG and NOTICE text into pieces, since CTCP is layered on top of these two commands. As well, in the sb_cleanup command earlier, a sbeDISCONNECT event is created. Similarly, an sbeCONNECT event is created in the next and last section.


The #sb_connecting group is enabled from the time you run sb_open to connect the bot to the time it considers itself successfully connected. It considers "successfully connected" to be the receipt of either the "end of MOTD" numeric (376) or the "MOTD not found" numeric (422). It also sends a new NICK command in the event that $sb_nick is taken -- $sb_nick2 should include something random because this event will retrigger until you find a nick that isn't taken. Lastly, upon receiving one of the aforementioned MOTD numerics, this section of the script sets the bot's "me" variable to the same one the server is sending its numerics to. This is because there is no answering "NICK" event when you first log in; this is the best way I could think of to tell which nick you connected as.

;group start
#sb_connecting off

;433 nick in use numeric -- send new nick to continue connecting
on *:signal:sbe_433:sb_out NICK $sb_nick2

;422 motd not found -- succesfully connected
on *:signal:sbe_422:sb_connected $3

;376 end of motd -- succesfully connected
on *:signal:sbe_376:sb_connected $3

;This alias does miscellaneous initialization upon connecting
alias -l sb_connected {

  ;sets the bot's "me" variable
  _me $1

  ;if the debug window is open, inform it that we are connected and with what nick
  if ($window(@sb_debug)) titlebar @sb_debug - Nick: $_me

  ;disable the group we are in
  .disable #sb_connecting

  ;send out a signal that we are connected
  .signal -n sbeCONNECT
}

;group end
#sb_connecting end


You will want to have a good understanding of the IRC protocol. The debug window can help with this, or you can do what I did when I was first figuring it out -- open up windows telnet and connect to your favorite server. See this link for the original RFC on the IRC protocol. Note that this document is almost 10 years old, and most or all of the major networks use IRCDs that expand upon it from a little to a lot. RFCs 2810, 2811, 2812, and 2813 are also available as "updates" to the original 1459. mirc.net also is a useful resource, it includes many numerics not mentioned in the original RFC but still used on lots of servers today. Take special note of numeric 005 as it gives you lots of useful information about the server's configuration.

A few last things of note:

This is a socket bot, not a mIRC server connection. This means that many mIRC identifiers will not work, including but not limited to the following:

$nick() $chan() $ial() $ibl() $address() $comchan() $me $notify

As well, some /if comparison operators will not work:

ison isop ishop isvoice isreg ischan isban

User levels will not apply (unless you specifically match the full nick!user@host string against your user list), and regular "mIRC events" will not work:

on TEXT, on NOTICE, on JOIN, on PART, on KICK, on QUIT, on NICK, ...

Especially of note is the fact that nick changes and quits are not associated with channels.

This all may sound somewhat daunting, but it's not really so bad; you will likely need to keep track of your status on channels, which channels you are on, etc, and other things. I do not recommend making a socket bot just because it sounds "elite" -- the truth is, it's a lot of work. A "regular" mIRC connection has many benefits, and now that mIRC supports multiple servers, there is not much reason to avoid doing things that way.

However, if you have a very specific purpose for your bot, sockets can be the most useful thing. You do not have to support anything more than what is necessary to do what your bot is supposed to; you will make it easy on yourself by figuring out what that is before anything else, and writing your script towards that goal.


Well! That was relatively concise for me, don't you think?

Before I go, I'll explain how to use this snippit to create a socket bot of your very own...

Step 1: Load the script ;)

Step 2: Change the identifiers sb_nick, sb_nick2, sb_user, and sb_realname to whatever you like. I recommend that you make $sb_nick2 return a nick with something sufficiently random in it that you can be sure it will not be taken on the IRC server you are connecting to.

Step 3: Start writing a bot script. I recommend you do this in a separate .mrc file altogether, it does not need to be in the same file as the rest of this. Use the "sb_open" command to connect your bot, and use the "sbe_*" or "sbe*" events to do things with it. Some examples that might come in handy:

;autojoin
on *:signal:sbeCONNECT:sb_out JOIN #chan1,#chan2,#chan3

;say stuff when you enter
on *:signal:sbe_JOIN:if ($_getnick($1) == $_me) sb_out PRIVMSG $_strip:($3) :n3v3r phj34r, j0r b0t 15 h34r!@$

;rejoin on kick
on *:signal:sbe_KICK:if ($4 == $_me) sb_out JOIN $3

Have fun!

All content is copyright by mircscripts.org and cannot be used without permission. For more details, click here.