Connect to an IRC server with Golang

How to connect to an IRC server like Freenode with Golang.

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 bot in 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 function and 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 :)