In the last time, I really interested in learn Golang.I like to learn while I code things and don’t read hundreds of blogs or books. So, here I show you, how I created a really simple IRC botin Golang.
IRC
Long before Discord, Slack or something else, we had IRC, and the best thing is, it’s still here. If you want to read more about IRC, here is the Wikipedia article.
IRC is an application layer protocol that facilitates communication in the form of text. The chat process works on a client/server networking model.
Lets start
In this blog, I don’t explain everything. The most things are really easy to teach it to yourself from the official documentation.
How the most projects, I started with the Golang skeleton:
package main
func main() {
}
Connect
The first thing that comes to mind is to connect to the server. In Golang, you can simple import a package called net
.
import (
"net"
)
Package
net
provides a portable interface for network I/O, including TCP/IP, UDP, domain name resolution, and Unix domain sockets.
The Dial function connects to a server, for example: net.Dial("tcp", "your_server:your_port")
.
You can read more information about net
and net.Dial
here: golang.org/pkg/net/
Now I added a new function and name it … connect()
… wow :D.
func connect() net.Conn {
conn, err := net.Dial("tcp", "your_server:your_port")
if err != nil {
panic(err)
}
return conn
}
Maybe, I can try to explain a bit here
func connect() net.Conn {
That’s just a function, net.Conn
is the return type.
conn, err := net.Dial("tcp", "your_server:your_port")
In Golang, we can return more than one values. So the first returned value is conn
and the second returned value is err
here.
if err != nil {
panic(err)
}
Simple error “handling”. panic(err)
abort and exit with a non-zero exit status.
When no error happened, we return the connection
return conn
Disconnect
Normally, when you connect to something you want also to disconnect. So I add another function and name it … disconnect()
… wow, again :).
func disconnect(conn net.Conn) {
conn.close()
}
I pass conn
into the function here and call simple conn.Close()
, to close the connection, obviously.
Now we can connect and disconnect to the server. Our code looks now like this:
package main
import (
"net"
)
func connect() net.Conn {
conn, err := net.Dial("tcp", "your_server:your_port")
if err != nil {
panic(err)
}
return conn
}
func disconnect(conn net.Conn) {
conn.Close()
}
func main() {
conn := connect()
disconnect(conn)
}
Read server messages
Somewhere between connect()
and disconnect()
we need to receive and sent messages. Like a smalltalk with people you don’t like. To read messages, we import three new packages: fmt
, bufio
, and net/textproto
.
Package fmt implements formatted I/O with functions analogous to C’s printf and scanf.
Package bufio implements buffered I/O.
Package textproto implements generic support for text-based request/response protocols in the style of HTTP, NNTP, and SMTP.
So we put all together and add the following code between connect()
and disconnect()
in our main()
function.
tp := textproto.NewReader(bufio.NewReader(conn))
for {
status, err := tp.ReadLine()
if err != nil {
panic(err)
}
fmt.Println(status)
}
for {}
is like an endless loop, read every line from conn
and print it out to our screen.
Now, we generate following output (I try to connect to Freenode in this example)
:hitchcock.freenode.net NOTICE * :*** Looking up your hostname...
:hitchcock.freenode.net NOTICE * :*** Checking Ident
:hitchcock.freenode.net NOTICE * :*** Couldn't look up your hostname
:hitchcock.freenode.net NOTICE * :*** No Ident response
So far so good, we can receive messages.
Talk with the server
After we can connect, read the server messages and disconnect, we need to “talk” with the server. In the real world, when we meet someone new we say our name … mostly. Same with our connection to the IRC server. The IRC protocol has two commands for this. USER
and NICK
.
So we need a function to say something like “Hi Server, I’m TheManWithTheIceCreamVan” or something else.We used already the package fmt
to print stuff to the screen, but we can use fmt
also, to send a message to the server. We need the function Fprintf
to do this.
For example: fmt.Fprintf(os.Stdout, "%s is %d years old.\n", name, age)
The first parameter of Fprintf can be our conn
variable, and the rest is just the string to “write”.
For example, we can create a function and calling it logon()
.
func logon(conn net.Conn) {
fmt.Fprintf(conn, "USER TheManWithTheIceCreamVan 8 * :Someone\r\n")
fmt.Fprintf(conn, "NICK TheManWithTheIceCreamVan\r\n")
}
At this point, we should directly create a helper function to send data to the server. We create a functionand name it sendData()
.
func sendData(conn net.Conn, message string) {
fmt.Fprintf(conn, "%s\r\n", message)
}
We simple pass the connection conn
, and the message to send to this function, the function itself calling fmt.Fprintf
then.
Back to our logon()
method, we use this new created function.
func logon(conn net.Conn) {
sendData(conn, "USER TheManWithTheIceCreamVan 8 * :Someone")
sendData(conn, "NICK TheManWithTheIceCreamVan")
}
After the connect()
line in our main()
function, we can call login(conn)
now. Our code looks now like this:
package main
import (
"net"
"fmt"
"net/textproto"
"bufio"
)
func connect() net.Conn {
conn, err := net.Dial("tcp", "irc.freenode.net:6667")
if err != nil {
panic(err)
}
return conn
}
func disconnect(conn net.Conn) {
conn.Close()
}
func logon(conn net.Conn) {
sendData(conn, "USER TheManWithTheIceCreamVan 8 * :Someone")
sendData(conn, "NICK TheManWithTheIceCreamVan")
}
func main() {
conn := connect()
logon(conn)
tp := textproto.NewReader(bufio.NewReader(conn))
for {
status, err := tp.ReadLine()
if err != nil {
panic(err)
}
fmt.Println(status)
}
disconnect(conn)
}
func sendData(conn net.Conn, message string) {
fmt.Fprintf(conn, "%s\r\n", message)
}
Now we got a lot of messages (MOTD, Stats, etc.) from the Freenode Server.
:tolkien.freenode.net NOTICE * :*** Looking up your hostname...
:tolkien.freenode.net NOTICE * :*** Checking Ident
:tolkien.freenode.net NOTICE * :*** No Ident response
:tolkien.freenode.net NOTICE * :*** Couldn't look up your hostname
:tolkien.freenode.net 001 TheManWithTheIce :Welcome to the freenode Internet Relay Chat Network TheManWithTheIce
:tolkien.freenode.net 002 TheManWithTheIce :Your host is tolkien.freenode.net[204.225.96.251/6667], running version ircd-seven-1.1.9
:tolkien.freenode.net 003 TheManWithTheIce :This server was created Fri Apr 24 2020 at 22:19:21 UTC
...
:tolkien.freenode.net 376 TheManWithTheIce :End of /MOTD command.
:TheManWithTheIce MODE TheManWithTheIce :+i
The last line means, we got USERMODE
+i from the IRC server. That’s because we requested this with the “8” in our USER
command:
sendData(conn, "USER TheManWithTheIceCreamVan 8 * :Someone")
Our Bot is connected to the server. When you /whois TheManWithTheIceCreamVan
from your regular IRC client you should get something like this
21:04 -!- TheManWithTheIce [~TheManWit@1.1.1.1]
21:04 -!- ircname : Someone
21:04 -!- server : hitchcock.freenode.net [Sofia, BG, EU]
21:04 -!- End of WHOIS
PONG
At regular intervals, the IRC server send you a “PING” message and when you don’t answer this message with a “PONG” message, the server will close the connection after a few seconds.
:TheManWithTheIce MODE TheManWithTheIce :+i
PING :hitchcock.freenode.net
:TheManWithTheIce!~TheManWit@37.228.165.230 QUIT :Ping timeout: 258 seconds
All we need is to check if we receive a message that start with “PING”. There’re different possibilities to do this, some people use regular expressions for this, we just use HasPrefix()
function from the strings
packages. For example:
if strings.HasPrefix("FOOBAR", "FOO") {
// FOOBAR start with FOO
} else {
// FOOBAR don't start with FOO
}
First, we could create a new method and name it … pong()
:)
func pong(conn net.Conn) {
sendData(conn, "PONG")
}
It’s just like the logon()
function. Everything we need to do is to send a simple “PONG” as message to the server.Now we can check the incoming messages and answer with our pong()
function:
func main() {
conn := connect()
logon(conn)
tp := textproto.NewReader(bufio.NewReader(conn))
for {
status, err := tp.ReadLine()
if err != nil {
panic(err)
}
fmt.Println(status)
if strings.HasPrefix(status, "PING") {
pong(conn)
}
}
disconnect(conn)
}
SIGTERM
You kill the bot mostly with an SIGTERM, like CTRL+C. To catch this and give the bot the possibility to for a clean disconnect()
we can implement the following:
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
disconnect(conn)
}()
… and add this to our main()
function
func main() {
conn := connect()
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
disconnect(conn)
}()
logon(conn)
tp := textproto.NewReader(bufio.NewReader(conn))
for {
status, err := tp.ReadLine()
if err != nil {
panic(err)
}
fmt.Println(status)
if strings.HasPrefix(status, "PING") {
pong(conn)
}
}
disconnect(conn)
}
QUIT
It’s a good manner to say goodbye politely. So before we hard disconnect()
from the server, we send the QUIT command
func disconnect(conn net.Conn) {
sendData(conn, "QUIT Bye")
conn.Close()
}
Full code
package main
import (
"bufio"
"fmt"
"net"
"net/textproto"
"os"
"os/signal"
"strings"
"syscall"
)
func connect() net.Conn {
conn, err := net.Dial("tcp", "irc.freenode.net:6667")
if err != nil {
panic(err)
}
return conn
}
func disconnect(conn net.Conn) {
sendData(conn, "QUIT Bye")
conn.Close()
}
func login(conn net.Conn) {
sendData(conn, "USER TheManWithTheIceCreamVan 8 * :Someone")
sendData(conn, "NICK TheManWithTheIceCreamVan")
}
func pong(conn net.Conn) {
sendData(conn, "PONG")
}
func main() {
conn := connect()
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
disconnect(conn)
}()
login(conn)
tp := textproto.NewReader(bufio.NewReader(conn))
for {
status, err := tp.ReadLine()
if err != nil {
panic(err)
}
fmt.Println(status)
if strings.HasPrefix(status, "PING") {
pong(conn)
}
}
}
func sendData(conn net.Conn, message string) {
fmt.Fprintf(conn, "%s\r\n", message)
}
Maybe, I will update this post sometime to show, how to join a channel and “talk” with other people :)
Top comments (0)