first commit
This commit is contained in:
commit
9eb8365acb
20
.travis.yml
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user