first commit

This commit is contained in:
Liam Stanley 2016-11-13 03:30:43 -05:00
commit 9eb8365acb
12 changed files with 1393 additions and 0 deletions

20
.travis.yml Normal file

@ -0,0 +1,20 @@
language: go
go:
- 1.7.1
- tip
script:
- go test
- go tool vet -v -all .
branches:
only:
- master
notifications:
irc:
channels:
- irc.byteirc.org#L
template:
- "%{repository} #%{build_number} %{branch}/%{commit}: %{author} -- %{message}
%{build_url}"
on_success: change
on_failure: change
skip_join: true

20
LICENSE Normal file

@ -0,0 +1,20 @@
LICENSE: The MIT License (MIT)
Copyright (c) 2016 Liam Stanley <me@liamstanley.io>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

18
README.md Normal file

@ -0,0 +1,18 @@
## girc is a flexible IRC library for Go
[![Build Status](https://travis-ci.org/Liamraystanley/girc.svg?branch=master)](https://travis-ci.org/Liamraystanley/girc)
[![GitHub Issues](https://img.shields.io/github/issues/Liamraystanley/girc.svg)](https://github.com/Liamraystanley/girc/issues)
[![GoDoc](https://godoc.org/github.com/Liamraystanley/girc?status.png)](https://godoc.org/github.com/Liamraystanley/girc)
[![codebeat badge](https://codebeat.co/badges/9899ad3d-23da-4f6b-84e1-78351e86e090)](https://codebeat.co/projects/github-com-liamraystanley-girc)
[![Go Report Card](https://goreportcard.com/badge/github.com/Liamraystanley/girc)](https://goreportcard.com/report/github.com/Liamraystanley/girc)
## Features
- Focuses on simplicity, yet tries to still be flexible
- Only requires standard packages
- Event based triggering/responses
- Documentation is actively being worked on
## Installing
$ go get -u github.com/Liamraystanley/girc

59
callback.go Normal file

@ -0,0 +1,59 @@
package girc
// handleEvent runs the necessary callbacks for the incoming event
func (c *Client) handleEvent(event *Event) {
// log the event
c.log.Print(event.String())
// wildcard callbacks first
if callbacks, ok := c.callbacks[ALLEVENTS]; ok {
for i := 0; i < len(callbacks); i++ {
callbacks[i].Execute(c, event)
}
}
// regular non-threaded callbacks
if callbacks, ok := c.callbacks[event.Command]; ok {
for i := 0; i < len(callbacks); i++ {
callbacks[i].Execute(c, event)
}
}
// callbacks that should be ran concurrently
// callbacks which should be ran in a go-routine should be prefixed with
// "routine_". e.g. "routine_JOIN".
if callbacks, ok := c.callbacks["routine_"+event.Command]; ok {
for i := 0; i < len(callbacks); i++ {
go callbacks[i].Execute(c, event)
}
}
}
// AddCallbackHandler registers the callback for the given command
func (c *Client) AddCallbackHandler(cmd string, callback Callback) {
c.callbacks[cmd] = append(c.callbacks[cmd], callback)
}
// AddCallback registers the callback function for the given command
func (c *Client) AddCallback(cmd string, callback func(c *Client, e *Event)) {
c.callbacks[cmd] = append(c.callbacks[cmd], CallbackFunc(callback))
}
// AddBgCallback registers the callback function for the given command
// and executes it in a go-routine, after all other callbacks have been ran
func (c *Client) AddBgCallback(cmd string, callback func(c *Client, e *Event)) {
c.callbacks["routine_"+cmd] = append(c.callbacks["routine_"+cmd], CallbackFunc(callback))
}
// Callback is an interface to handle IRC events
type Callback interface {
Execute(*Client, *Event)
}
// CallbackFunc is a type that represents the function necessary to implement Callback
type CallbackFunc func(c *Client, e *Event)
// Execute calls the CallbackFunc with the sender and irc message
func (f CallbackFunc) Execute(c *Client, e *Event) {
f(c, e)
}

112
conn.go Normal file

@ -0,0 +1,112 @@
package girc
import (
"bufio"
"io"
"net"
"sync"
)
// messages are delimited with CR and LF line endings, we're using the last
// one to split the stream. both are removed during parsing of the message.
const delim byte = '\n'
var endline = []byte("\r\n")
// Conn represents an IRC network protocol connection, it consists of an
// Encoder and Decoder to manage i/o
type Conn struct {
Encoder
Decoder
conn io.ReadWriteCloser
}
// NewConn returns a new Conn using rwc for i/o
func NewConn(rwc io.ReadWriteCloser) *Conn {
return &Conn{
Encoder: Encoder{
writer: rwc,
},
Decoder: Decoder{
reader: bufio.NewReader(rwc),
},
conn: rwc,
}
}
// Dial connects to the given address using net.Dial and then returns a
// new Conn for the connection
func Dial(addr string) (*Conn, error) {
c, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
return NewConn(c), nil
}
// Close closes the underlying ReadWriteCloser
func (c *Conn) Close() error {
return c.conn.Close()
}
// A Decoder reads Event objects from an input stream
type Decoder struct {
reader *bufio.Reader
line string
mu sync.Mutex
}
// NewDecoder returns a new Decoder that reads from r
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{reader: bufio.NewReader(r)}
}
// Decode attempts to read a single Event from the stream, returns non-nil
// error if read failed
func (dec *Decoder) Decode() (e *Event, err error) {
dec.mu.Lock()
dec.line, err = dec.reader.ReadString(delim)
dec.mu.Unlock()
if err != nil {
return nil, err
}
return ParseEvent(dec.line), nil
}
// Encoder writes Event objects to an output stream
type Encoder struct {
writer io.Writer
mu sync.Mutex
}
// NewEncoder returns a new Encoder that writes to w
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{writer: w}
}
// Encode writes the IRC encoding of m to the stream. goroutine safe.
// returns non-nil error if the write to the underlying stream stopped early.
func (enc *Encoder) Encode(e *Event) (err error) {
_, err = enc.Write(e.Bytes())
return
}
// Write writes len(p) bytes from p followed by CR+LF. goroutine safe.
func (enc *Encoder) Write(p []byte) (n int, err error) {
enc.mu.Lock()
n, err = enc.writer.Write(p)
if err != nil {
enc.mu.Unlock()
return
}
_, err = enc.writer.Write(endline)
enc.mu.Unlock()
return
}

299
contants.go Normal file

@ -0,0 +1,299 @@
package girc
// misc constants for use with the client
const (
ALLEVENTS = "*" // trigger on all events
CONNECTED = "CONNECTED" // event command which can be used to start responding, after SUCCESS
SUCCESS = "001" // RPL_WELCOME alias, assumes successful connection
)
// user/channel prefixes :: RFC1459
const (
ChannelPrefix = "#" // regular channel
DistributedPrefix = "&" // distributed channel
OwnerPrefix = "~" // user owner +q (non-rfc)
AdminPrefix = "&" // user admin +a (non-rfc)
HalfOperatorPrefix = "%" // user half operator +h (non-rfc)
OperatorPrefix = "@" // user operator +o
VoicePrefix = "+" // user has voice +v
)
// user modes :: RFC1459; section 4.2.3.2
const (
UserModeInvisible = "i" // invisible
UserModeOperator = "o" // server operator
UserModeServerNotices = "s" // user wants to receive server notices
UserModeWallops = "w" // user wants to receive wallops
)
// channel modes :: RFC1459; section 4.2.3.1
const (
ModeAdmin = "a" // admin privileges (non-rfc)
ModeHalfOperator = "h" // half-operator privileges (non-rfc)
ModeInviteOnly = "i" // only join with an invite
ModeKey = "k" // channel password
ModeLimit = "l" // user limit
ModeModerated = "m" // only voiced users and operators can talk
ModeOperator = "o" // operator
ModeOwner = "q" // owner privileges (non-rfc)
ModePrivate = "p" // private
ModeSecret = "s" // secret
ModeTopic = "t" // must be op to set topic
ModeVoice = "v" // speak during moderation mode
)
// irc commands :: RFC2812; section 3 :: RFC2813; section 4
const (
ADMIN = "ADMIN"
AWAY = "AWAY"
CONNECT = "CONNECT"
DIE = "DIE"
ERROR = "ERROR"
INFO = "INFO"
INVITE = "INVITE"
ISON = "ISON"
JOIN = "JOIN"
KICK = "KICK"
KILL = "KILL"
LINKS = "LINKS"
LIST = "LIST"
LUSERS = "LUSERS"
MODE = "MODE"
MOTD = "MOTD"
NAMES = "NAMES"
NICK = "NICK"
NJOIN = "NJOIN"
NOTICE = "NOTICE"
OPER = "OPER"
PART = "PART"
PASS = "PASS"
PING = "PING"
PONG = "PONG"
PRIVMSG = "PRIVMSG"
QUIT = "QUIT"
REHASH = "REHASH"
RESTART = "RESTART"
SERVER = "SERVER"
SERVICE = "SERVICE"
SERVLIST = "SERVLIST"
SQUERY = "SQUERY"
SQUIT = "SQUIT"
STATS = "STATS"
SUMMON = "SUMMON"
TIME = "TIME"
TOPIC = "TOPIC"
TRACE = "TRACE"
USER = "USER"
USERHOST = "USERHOST"
USERS = "USERS"
VERSION = "VERSION"
WALLOPS = "WALLOPS"
WHO = "WHO"
WHOIS = "WHOIS"
WHOWAS = "WHOWAS"
)
// numeric IRC reply mapping :: RFC2812; section 5
const (
RPL_WELCOME = "001"
RPL_YOURHOST = "002"
RPL_CREATED = "003"
RPL_MYINFO = "004"
RPL_BOUNCE = "005"
RPL_ISUPPORT = "005"
RPL_USERHOST = "302"
RPL_ISON = "303"
RPL_AWAY = "301"
RPL_UNAWAY = "305"
RPL_NOWAWAY = "306"
RPL_WHOISUSER = "311"
RPL_WHOISSERVER = "312"
RPL_WHOISOPERATOR = "313"
RPL_WHOISIDLE = "317"
RPL_ENDOFWHOIS = "318"
RPL_WHOISCHANNELS = "319"
RPL_WHOWASUSER = "314"
RPL_ENDOFWHOWAS = "369"
RPL_LISTSTART = "321"
RPL_LIST = "322"
RPL_LISTEND = "323"
RPL_UNIQOPIS = "325"
RPL_CHANNELMODEIS = "324"
RPL_NOTOPIC = "331"
RPL_TOPIC = "332"
RPL_INVITING = "341"
RPL_SUMMONING = "342"
RPL_INVITELIST = "346"
RPL_ENDOFINVITELIST = "347"
RPL_EXCEPTLIST = "348"
RPL_ENDOFEXCEPTLIST = "349"
RPL_VERSION = "351"
RPL_WHOREPLY = "352"
RPL_ENDOFWHO = "315"
RPL_NAMREPLY = "353"
RPL_ENDOFNAMES = "366"
RPL_LINKS = "364"
RPL_ENDOFLINKS = "365"
RPL_BANLIST = "367"
RPL_ENDOFBANLIST = "368"
RPL_INFO = "371"
RPL_ENDOFINFO = "374"
RPL_MOTDSTART = "375"
RPL_MOTD = "372"
RPL_ENDOFMOTD = "376"
RPL_YOUREOPER = "381"
RPL_REHASHING = "382"
RPL_YOURESERVICE = "383"
RPL_TIME = "391"
RPL_USERSSTART = "392"
RPL_USERS = "393"
RPL_ENDOFUSERS = "394"
RPL_NOUSERS = "395"
RPL_TRACELINK = "200"
RPL_TRACECONNECTING = "201"
RPL_TRACEHANDSHAKE = "202"
RPL_TRACEUNKNOWN = "203"
RPL_TRACEOPERATOR = "204"
RPL_TRACEUSER = "205"
RPL_TRACESERVER = "206"
RPL_TRACESERVICE = "207"
RPL_TRACENEWTYPE = "208"
RPL_TRACECLASS = "209"
RPL_TRACERECONNECT = "210"
RPL_TRACELOG = "261"
RPL_TRACEEND = "262"
RPL_STATSLINKINFO = "211"
RPL_STATSCOMMANDS = "212"
RPL_ENDOFSTATS = "219"
RPL_STATSUPTIME = "242"
RPL_STATSOLINE = "243"
RPL_UMODEIS = "221"
RPL_SERVLIST = "234"
RPL_SERVLISTEND = "235"
RPL_LUSERCLIENT = "251"
RPL_LUSEROP = "252"
RPL_LUSERUNKNOWN = "253"
RPL_LUSERCHANNELS = "254"
RPL_LUSERME = "255"
RPL_ADMINME = "256"
RPL_ADMINLOC1 = "257"
RPL_ADMINLOC2 = "258"
RPL_ADMINEMAIL = "259"
RPL_TRYAGAIN = "263"
ERR_NOSUCHNICK = "401"
ERR_NOSUCHSERVER = "402"
ERR_NOSUCHCHANNEL = "403"
ERR_CANNOTSENDTOCHAN = "404"
ERR_TOOMANYCHANNELS = "405"
ERR_WASNOSUCHNICK = "406"
ERR_TOOMANYTARGETS = "407"
ERR_NOSUCHSERVICE = "408"
ERR_NOORIGIN = "409"
ERR_NORECIPIENT = "411"
ERR_NOTEXTTOSEND = "412"
ERR_NOTOPLEVEL = "413"
ERR_WILDTOPLEVEL = "414"
ERR_BADMASK = "415"
ERR_UNKNOWNCOMMAND = "421"
ERR_NOMOTD = "422"
ERR_NOADMININFO = "423"
ERR_FILEERROR = "424"
ERR_NONICKNAMEGIVEN = "431"
ERR_ERRONEUSNICKNAME = "432"
ERR_NICKNAMEINUSE = "433"
ERR_NICKCOLLISION = "436"
ERR_UNAVAILRESOURCE = "437"
ERR_USERNOTINCHANNEL = "441"
ERR_NOTONCHANNEL = "442"
ERR_USERONCHANNEL = "443"
ERR_NOLOGIN = "444"
ERR_SUMMONDISABLED = "445"
ERR_USERSDISABLED = "446"
ERR_NOTREGISTERED = "451"
ERR_NEEDMOREPARAMS = "461"
ERR_ALREADYREGISTRED = "462"
ERR_NOPERMFORHOST = "463"
ERR_PASSWDMISMATCH = "464"
ERR_YOUREBANNEDCREEP = "465"
ERR_YOUWILLBEBANNED = "466"
ERR_KEYSET = "467"
ERR_CHANNELISFULL = "471"
ERR_UNKNOWNMODE = "472"
ERR_INVITEONLYCHAN = "473"
ERR_BANNEDFROMCHAN = "474"
ERR_BADCHANNELKEY = "475"
ERR_BADCHANMASK = "476"
ERR_NOCHANMODES = "477"
ERR_BANLISTFULL = "478"
ERR_NOPRIVILEGES = "481"
ERR_CHANOPRIVSNEEDED = "482"
ERR_CANTKILLSERVER = "483"
ERR_RESTRICTED = "484"
ERR_UNIQOPPRIVSNEEDED = "485"
ERR_NOOPERHOST = "491"
ERR_UMODEUNKNOWNFLAG = "501"
ERR_USERSDONTMATCH = "502"
)
// ircv3 commands :: http://ircv3.net/irc/
const (
AUTHENTICATE = "AUTHENTICATE"
CAP = "CAP"
CAP_ACK = "ACK"
CAP_CLEAR = "CLEAR"
CAP_END = "END"
CAP_LIST = "LIST"
CAP_LS = "LS"
CAP_NAK = "NAK"
CAP_REQ = "REQ"
)
// numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/
const (
RPL_LOGGEDIN = "900"
RPL_LOGGEDOUT = "901"
RPL_NICKLOCKED = "902"
RPL_SASLSUCCESS = "903"
ERR_SASLFAIL = "904"
ERR_SASLTOOLONG = "905"
ERR_SASLABORTED = "906"
ERR_SASLALREADY = "907"
RPL_SASLMECHS = "908"
)
// numeric IRC event mapping :: RFC2812; section 5.3
const (
RPL_STATSCLINE = "213"
RPL_STATSNLINE = "214"
RPL_STATSILINE = "215"
RPL_STATSKLINE = "216"
RPL_STATSQLINE = "217"
RPL_STATSYLINE = "218"
RPL_SERVICEINFO = "231"
RPL_ENDOFSERVICES = "232"
RPL_SERVICE = "233"
RPL_STATSVLINE = "240"
RPL_STATSLLINE = "241"
RPL_STATSHLINE = "244"
RPL_STATSSLINE = "245"
RPL_STATSPING = "246"
RPL_STATSBLINE = "247"
RPL_STATSDLINE = "250"
RPL_NONE = "300"
RPL_WHOISCHANOP = "316"
RPL_KILLDONE = "361"
RPL_CLOSING = "362"
RPL_CLOSEEND = "363"
RPL_INFOSTART = "373"
RPL_MYPORTIS = "384"
ERR_NOSERVICEHOST = "492"
)
// misc.
const (
ERR_TOOMANYMATCHES = "416" // IRCNet
RPL_GLOBALUSERS = "266" // aircd/hybrid/bahamut, used on freenode
RPL_LOCALUSERS = "265" // aircd/hybrid/bahamut, used on freenode
RPL_TOPICWHOTIME = "333" // ircu, in use on Freenode
RPL_WHOSPCRPL = "354" // ircu, used on networks with WHOX support
)

306
event.go Normal file

@ -0,0 +1,306 @@
package girc
import (
"bytes"
"strings"
)
const (
prefix byte = 0x3A // prefix or last argument
prefixUser byte = 0x21 // username
prefixHost byte = 0x40 // hostname
space byte = 0x20 // separator
maxLength = 510 // maximum length is 510 (2 for line endings)
)
func cutsetFunc(r rune) bool {
// Characters to trim from prefixes/messages.
return r == '\r' || r == '\n'
}
// Prefix represents the sender of an IRC event, see RFC1459 section 2.3.1
// <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
type Prefix struct {
Name string // Nick or servername
User string // Username
Host string // Hostname
}
// ParsePrefix takes a string and attempts to create a Prefix struct.
func ParsePrefix(raw string) (p *Prefix) {
p = new(Prefix)
user := indexByte(raw, prefixUser)
host := indexByte(raw, prefixHost)
switch {
case user > 0 && host > user:
p.Name = raw[:user]
p.User = raw[user+1 : host]
p.Host = raw[host+1:]
case user > 0:
p.Name = raw[:user]
p.User = raw[user+1:]
case host > 0:
p.Name = raw[:host]
p.Host = raw[host+1:]
default:
p.Name = raw
}
return p
}
// Len calculates the length of the string representation of prefix
func (p *Prefix) Len() (length int) {
length = len(p.Name)
if len(p.User) > 0 {
length = 1 + length + len(p.User)
}
if len(p.Host) > 0 {
length = 1 + length + len(p.Host)
}
return
}
// Bytes returns a []byte representation of prefix
func (p *Prefix) Bytes() []byte {
buffer := new(bytes.Buffer)
p.writeTo(buffer)
return buffer.Bytes()
}
// String returns a string representation of prefix
func (p *Prefix) String() (s string) {
s = p.Name
if len(p.User) > 0 {
s = s + string(prefixUser) + p.User
}
if len(p.Host) > 0 {
s = s + string(prefixHost) + p.Host
}
return
}
// IsHostmask returns true if prefix looks like a user hostmask
func (p *Prefix) IsHostmask() bool {
return len(p.User) > 0 && len(p.Host) > 0
}
// IsServer returns true if this prefix looks like a server name.
func (p *Prefix) IsServer() bool {
return len(p.User) <= 0 && len(p.Host) <= 0 // && indexByte(p.Name, '.') > 0
}
// writeTo is an utility function to write the prefix to the bytes.Buffer in Event.String()
func (p *Prefix) writeTo(buffer *bytes.Buffer) {
buffer.WriteString(p.Name)
if len(p.User) > 0 {
buffer.WriteByte(prefixUser)
buffer.WriteString(p.User)
}
if len(p.Host) > 0 {
buffer.WriteByte(prefixHost)
buffer.WriteString(p.Host)
}
return
}
// Event represents an IRC protocol message, see RFC1459 section 2.3.1
//
// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf>
// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>]
// <command> :: <letter>{<letter>} | <number> <number> <number>
// <SPACE> :: ' '{' '}
// <params> :: <SPACE> [':' <trailing> | <middle> <params>]
// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL
// or CR or LF, the first of which may not be ':'>
// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or
// CR or LF>
// <crlf> :: CR LF
type Event struct {
*Prefix
Command string
Params []string
Trailing string
// When set to true, the trailing prefix (:) will be added even if the trailing message is empty.
EmptyTrailing bool
Sensitive bool // if the message is sensitive (e.g. and should not be logged)
}
// ParseEvent takes a string and attempts to create a Event struct.
// Returns nil if the Event is invalid.
func ParseEvent(raw string) (e *Event) {
// ignore empty events
if raw = strings.TrimFunc(raw, cutsetFunc); len(raw) < 2 {
return nil
}
i, j := 0, 0
e = new(Event)
if raw[0] == prefix {
// prefix ends with a space
i = indexByte(raw, space)
// prefix string must not be empty if the indicator is present
if i < 2 {
return nil
}
e.Prefix = ParsePrefix(raw[1:i])
// skip space at the end of the prefix
i++
}
// find end of command
j = i + indexByte(raw[i:], space)
// extract command
if j > i {
e.Command = strings.ToUpper(raw[i:j])
} else {
e.Command = strings.ToUpper(raw[i:])
return e
}
// skip space after command
j++
// find prefix for trailer
i = indexByte(raw[j:], prefix)
if i < 0 || raw[j+i-1] != space {
// no trailing argument
e.Params = strings.Split(raw[j:], string(space))
return e
}
// compensate for index on substring
i = i + j
// check if we need to parse arguments
if i > j {
e.Params = strings.Split(raw[j:i-1], string(space))
}
e.Trailing = raw[i+1:]
// we need to re-encode the trailing argument even if it was empty
if len(e.Trailing) <= 0 {
e.EmptyTrailing = true
}
return e
}
// Len calculates the length of the string representation of this event
func (e *Event) Len() (length int) {
if e.Prefix != nil {
length = e.Prefix.Len() + 2 // include prefix and trailing space
}
length = length + len(e.Command)
if len(e.Params) > 0 {
length = length + len(e.Params)
for _, param := range e.Params {
length = length + len(param)
}
}
if len(e.Trailing) > 0 || e.EmptyTrailing {
length = length + len(e.Trailing) + 2 // include prefix and space
}
return
}
// Bytes returns a []byte representation of this event
//
// as noted in RFC2812 section 2.3, messages should not exceed 512 characters
// in length. this method forces that limit by discarding any characters
// exceeding the length limit.
func (e *Event) Bytes() []byte {
buffer := new(bytes.Buffer)
// event prefix
if e.Prefix != nil {
buffer.WriteByte(prefix)
e.Prefix.writeTo(buffer)
buffer.WriteByte(space)
}
// command is required
buffer.WriteString(e.Command)
// space separated list of arguments
if len(e.Params) > 0 {
buffer.WriteByte(space)
buffer.WriteString(strings.Join(e.Params, string(space)))
}
if len(e.Trailing) > 0 || e.EmptyTrailing {
buffer.WriteByte(space)
buffer.WriteByte(prefix)
buffer.WriteString(e.Trailing)
}
// we need the limit the buffer length
if buffer.Len() > (maxLength) {
buffer.Truncate(maxLength)
}
return buffer.Bytes()
}
// String returns a string representation of this event
func (e *Event) String() string {
return string(e.Bytes())
}
func indexByte(s string, c byte) int {
return strings.IndexByte(s, c)
}
// contains '*', even though this isn't RFC compliant, it's commonly used
var validChannelPrefixes = [...]string{"&", "#", "+", "!", "*"}
// IsValidChannel checks if channel is an RFC complaint channel or not
func IsValidChannel(channel string) bool {
if len(channel) < 1 || len(channel) > 50 {
return false
}
var validprefix bool
for i := 0; i < len(validChannelPrefixes); i++ {
if string(channel[0]) == validChannelPrefixes[i] {
validprefix = true
break
}
}
if !validprefix {
return false
}
if strings.Contains(channel, " ") || strings.Contains(channel, ",") {
return false
}
return true
}

34
example/main.go Normal file

@ -0,0 +1,34 @@
package main
import (
"log"
"os"
"github.com/Liamraystanley/girc"
)
func main() {
conf := girc.Config{
Server: "irc.byteirc.org",
Port: 6667,
Nick: "test",
User: "test1",
Name: "Example bot",
MaxRetries: 3,
Logger: os.Stdout,
}
client := girc.New(conf)
client.AddCallback(girc.CONNECTED, registerConnect)
if err := client.Connect(); err != nil {
log.Fatalf("an error occurred while attempting to connect: %s", err)
}
client.Wait()
}
func registerConnect(c *girc.Client, e *girc.Event) {
c.Send(&girc.Event{Command: girc.JOIN, Params: []string{"#dev"}})
}

114
helpers.go Normal file

@ -0,0 +1,114 @@
package girc
import (
"time"
"github.com/y0ssar1an/q"
)
func (c *Client) registerHelpers() {
c.AddBgCallback(SUCCESS, handleWelcome)
c.AddCallback(PING, handlePING)
// joins/parts/anything that may add/remove users
c.AddCallback(JOIN, handleJOIN)
c.AddCallback(PART, handlePART)
c.AddCallback(KICK, handleKICK)
// WHO/WHOX responses
c.AddCallback(RPL_WHOREPLY, handleWHO)
c.AddCallback(RPL_WHOSPCRPL, handleWHO)
// nickname collisions
c.AddCallback(ERR_NICKNAMEINUSE, nickCollisionHandler)
c.AddCallback(ERR_NICKCOLLISION, nickCollisionHandler)
c.AddCallback(ERR_UNAVAILRESOURCE, nickCollisionHandler)
}
// handleWelcome is a helper function which lets the client know
// that enough time has passed and now they can send commands
//
// should always run in separate thread
func handleWelcome(c *Client, e *Event) {
// this should be the nick that the server gives us. 99% of the time, it's the
// one we supplied during connection, but some networks will insta-rename users.
if len(e.Params) > 0 {
c.State.nick = e.Params[0]
}
time.Sleep(2 * time.Second)
c.Events <- &Event{Command: CONNECTED}
}
// nickCollisionHandler helps prevent the client from having conflicting
// nicknames with another bot, user, etc
func nickCollisionHandler(c *Client, e *Event) {
c.SetNick(c.GetNick() + "_")
}
// handlePING helps respond to ping requests from the server
func handlePING(c *Client, e *Event) {
// TODO: we should be sending pings too.
c.Send(&Event{Command: PONG, Params: e.Params, Trailing: e.Trailing})
}
// handleJOIN ensures that the state has updated users and channels
func handleJOIN(c *Client, e *Event) {
if len(e.Params) == 0 {
return
}
// create it in state
c.State.createChanIfNotExists(e.Params[0])
c.Who(e.Params[0])
}
// handlePART ensures that the state is clean of old user and channel entries
func handlePART(c *Client, e *Event) {
if len(e.Params) == 0 {
return
}
if e.Prefix.Name == c.GetNick() {
c.State.deleteChannel(e.Params[0])
return
}
c.State.deleteUser(e.Params[0], e.Prefix.Name)
}
func handleWHO(c *Client, e *Event) {
var channel, user, host, nick string
// assume WHOX related
if e.Command == RPL_WHOSPCRPL {
if len(e.Params) != 6 {
// assume there was some form of error or invalid WHOX response
return
}
if e.Params[1] != "1" {
// we should always be sending 1, and we should receive 1. if this
// is anything but, then we didn't send the request and we can
// ignore it.
return
}
channel, user, host, nick = e.Params[2], e.Params[3], e.Params[4], e.Params[5]
} else {
channel, user, host, nick = e.Params[1], e.Params[2], e.Params[3], e.Params[5]
}
c.State.createUserIfNotExists(channel, nick, user, host)
}
func handleKICK(c *Client, e *Event) {
if len(e.Params) < 2 {
// needs at least channel and user
return
}
q.Q(e)
}

262
main.go Normal file

@ -0,0 +1,262 @@
package girc
import (
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"net"
"time"
)
// TODO: See all todos!
// Client contains all of the information necessary to run a single IRC client
type Client struct {
Config Config // configuration for client
State *State // state for the client
Events chan *Event // queue of events to handle
Sender Sender // send wrapper for conn
initTime time.Time // time when the client was created
callbacks map[string][]Callback // mapping of callbacks
reader *Decoder // for use with reading from conn stream
writer *Encoder // for use with writing to conn stream
conn net.Conn // network connection to the irc server
tries int // number of attempts to connect to the server
log *log.Logger // package logger
quitChan chan bool // channel used for disconnect/quitting
}
// Config contains configuration options for an IRC client
type Config struct {
Server string // server to connect to
Port int // port to use for server
Password string // password for the irc server
Nick string // nickname to attempt to use on connect
User string // username to attempt to use on connect
Name string // "realname" to attempt to use on connect
TLSConfig *tls.Config // tls/ssl configuration
MaxRetries int // max number of reconnect retries
Logger io.Writer // writer for which to write logs to
DisableHelpers bool // if default event handlers should be used (to respond to ping, user tracking, etc)
}
// New creates a new IRC client with the specified server, name and config
func New(config Config) *Client {
client := &Client{
Config: config,
Events: make(chan *Event, 10), // buffer 10 events
quitChan: make(chan bool),
callbacks: make(map[string][]Callback),
tries: 0,
initTime: time.Now(),
}
// register builtin helpers
if !client.Config.DisableHelpers {
client.registerHelpers()
}
return client
}
// Quit disconnects from the server
func (c *Client) Quit() {
// TODO: sent QUIT?
if c.conn != nil {
c.conn.Close()
}
c.quitChan <- true
}
// Uptime returns the amount of time that has passed since the client was created
func (c *Client) Uptime() time.Duration {
return time.Now().Sub(c.initTime)
}
// Server returns the string representation of host+port pair for net.Conn
func (c *Client) Server() string {
return fmt.Sprintf("%s:%d", c.Config.Server, c.Config.Port)
}
// Send is a handy wrapper around Sender
func (c *Client) Send(event *Event) error {
// log the event
if !event.Sensitive {
c.log.Print("[write] ", event.String())
}
return c.Sender.Send(event)
}
// Connect attempts to connect to the given IRC server
func (c *Client) Connect() error {
var conn net.Conn
var err error
// sanity check a few things here...
if c.Config.Server == "" || c.Config.Port == 0 || c.Config.Nick == "" || c.Config.User == "" {
return errors.New("invalid configuration (server/port/nick/user)")
}
// reset our state here
c.State = NewState()
if c.Config.Logger == nil {
c.Config.Logger = ioutil.Discard
}
c.log = log.New(c.Config.Logger, "", log.Ldate|log.Ltime|log.Lshortfile)
if c.Config.TLSConfig == nil {
conn, err = net.Dial("tcp", c.Server())
} else {
conn, err = tls.Dial("tcp", c.Server(), c.Config.TLSConfig)
}
if err != nil {
return err
}
c.conn = conn
c.reader = NewDecoder(conn)
c.writer = NewEncoder(conn)
c.Sender = serverSender{writer: c.writer}
for _, event := range c.connectMessages() {
if err := c.Send(event); err != nil {
return err
}
}
c.tries = 0
go c.ReadLoop()
// consider the connection a success at this point
c.State.connected = true
return nil
}
// connectMessages is a list of IRC messages to send when attempting to
// connect to the IRC server.
func (c *Client) connectMessages() []*Event {
events := []*Event{}
// passwords first
if c.Config.Password != "" {
events = append(events, &Event{Command: PASS, Params: []string{c.Config.Password}})
}
// send nickname
events = append(events, &Event{Command: NICK, Params: []string{c.Config.Nick}})
// then username and realname
if c.Config.Name == "" {
c.Config.Name = c.Config.User
}
events = append(events, &Event{
Command: USER,
Params: []string{c.Config.User, "+iw", "*"},
Trailing: c.Config.Name,
})
return events
}
// Reconnect checks to make sure we want to, and then attempts to
// reconnect to the server
func (c *Client) Reconnect() error {
if c.Config.MaxRetries > 0 {
c.conn.Close()
var err error
// sleep for 10 seconds so we're not slaughtering the server
c.log.Printf("reconnecting to %s in 10 seconds", c.Server())
time.Sleep(10 * time.Second)
for err = c.Connect(); err != nil && c.tries < c.Config.MaxRetries; c.tries++ {
duration := time.Duration(math.Pow(2.0, float64(c.tries))*200) * time.Millisecond
time.Sleep(duration)
}
return err
}
close(c.Events)
return nil
}
// ReadLoop sets a timeout of 300 seconds, and then attempts to read
// from the IRC server. If there is an error, it calls Reconnect
func (c *Client) ReadLoop() error {
for {
c.conn.SetDeadline(time.Now().Add(300 * time.Second))
event, err := c.reader.Decode()
if err != nil {
return c.Reconnect()
}
// TODO: not adding PRIVMSG entries?
c.Events <- event
}
}
// Wait reads from the events channel and sends the events to be handled
// for every message it recieves.
func (c *Client) Wait() {
var e *Event
for {
select {
case e = <-c.Events:
c.handleEvent(e)
case <-c.quitChan:
return
}
}
}
// IsConnected returns true if the client is connected to the server
func (c *Client) IsConnected() bool {
c.State.m.RLock()
defer c.State.m.RUnlock()
return c.State.connected
}
// GetNick returns the current nickname of the active connection
func (c *Client) GetNick() string {
c.State.m.RLock()
defer c.State.m.RUnlock()
if c.State.nick == "" {
return c.Config.Nick
}
return c.State.nick
}
// SetNick changes the client nickname
func (c *Client) SetNick(name string) {
c.State.m.Lock()
defer c.State.m.Unlock()
c.State.nick = name
c.Send(&Event{Command: NICK, Params: []string{name}})
}
func (c *Client) GetChannels() map[string]*Channel {
c.State.m.RLock()
defer c.State.m.RUnlock()
return c.State.channels
}
// Who tells the client to update it's channel/user records
func (c *Client) Who(channel string) {
c.Send(&Event{Command: WHO, Params: []string{channel, "%tcuhn,1"}})
}

18
sender.go Normal file

@ -0,0 +1,18 @@
package girc
// Sender is an interface for sending IRC messages
type Sender interface {
// Send sends the given message and returns any errors.
Send(*Event) error
}
// serverSender is a barebones writer used
// as the default sender for all callbacks
type serverSender struct {
writer *Encoder
}
// Send sends the specified event
func (s serverSender) Send(event *Event) error {
return s.writer.Encode(event)
}

131
state.go Normal file

@ -0,0 +1,131 @@
package girc
import (
"strings"
"sync"
"time"
)
// TODO: conntime, uptime
// State represents the actively-changing variables within the client runtime
type State struct {
m sync.RWMutex // lock, primarily used for writing things in state
connected bool // if we're connected to the server or not
nick string // internal tracker for our nickname
channels map[string]*Channel // map of channels that the client is in
}
// User represents an IRC user and the state attached to them
type User struct {
Nick string // nickname of the user
Ident string // ident (often referred to as "user") of the user
Host string // host that server is providing for the user, may not always be accurate
FirstSeen time.Time // the first time they were seen by the client
}
// Channel represents an IRC channel and the state attached to it
type Channel struct {
// TODO: users needs to be exposed
Name string // name of the channel, always lowercase
users map[string]*User
Joined time.Time // when the channel was joined
}
// NewState returns a clean state
func NewState() *State {
s := &State{}
s.channels = make(map[string]*Channel)
s.connected = false
return s
}
// createChanIfNotExists creates the channel in state, if not already done
func (s *State) createChanIfNotExists(channel string) {
s.m.Lock()
defer s.m.Unlock()
// not a valid channel
if !IsValidChannel(channel) {
return
}
if _, ok := s.channels[channel]; !ok {
s.channels[channel] = &Channel{
Name: strings.ToLower(channel),
users: make(map[string]*User),
Joined: time.Now(),
}
}
}
// deleteChannel removes the channel from state, if not already done
func (s *State) deleteChannel(channel string) {
s.m.Lock()
defer s.m.Unlock()
s.createChanIfNotExists(channel)
if _, ok := s.channels[channel]; ok {
delete(s.channels, channel)
}
}
// createUserIfNotExists creates the channel and user in state,
// if not already done
func (s *State) createUserIfNotExists(channel, nick, ident, host string) {
s.m.Lock()
defer s.m.Unlock()
s.createChanIfNotExists(channel)
if _, ok := s.channels[channel].users[nick]; !ok {
s.channels[channel].users[nick] = &User{
Nick: nick,
Ident: ident,
Host: host,
FirstSeen: time.Now(),
}
}
}
// deleteUser removes the user from channel state
func (s *State) deleteUser(channel, nick string) {
s.m.Lock()
defer s.m.Unlock()
s.createChanIfNotExists(channel)
if _, ok := s.channels[channel].users[nick]; ok {
delete(s.channels[channel].users, nick)
}
}
// renameUser renames the user in state, in all locations where
// relevant
func (s *State) renameUser(from, to string) {
s.m.Lock()
defer s.m.Unlock()
for k := range s.channels {
// check to see if they're in this channel
if _, ok := s.channels[k].users[from]; !ok {
return
}
// take the actual reference to the pointer
source := *s.channels[k].users[from]
// update the nick field (as we not only have a key, but a
// matching struct field)
source.Nick = to
// delete the old
delete(s.channels[k].users, from)
// in with the new
s.channels[k].users[to] = &source
}
}