pax_global_header00006660000000000000000000000064124641556340014524gustar00rootroot0000000000000052 comment=c7e4fe6bc17a3d2a70993dea7ff96026cb964d7d irc-1.1.0/000077500000000000000000000000001246415563400123005ustar00rootroot00000000000000irc-1.1.0/LICENSE000066400000000000000000000020341246415563400133040ustar00rootroot00000000000000Copyright 2014 Vic Demuzere 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. irc-1.1.0/README.md000066400000000000000000000051041246415563400135570ustar00rootroot00000000000000# Go **irc** package [Package Documentation][Documentation] @ godoc.org [![Build Status](https://drone.io/github.com/sorcix/irc/status.png)](https://drone.io/github.com/sorcix/irc/latest) ## Features Package irc allows your application to speak the IRC protocol. - **Limited scope**, does one thing and does it well. - Focus on simplicity and **speed**. - **Stable API**: updates shouldn't break existing software. - Well [documented][Documentation] code. *This package does not manage your entire IRC connection. It only translates the protocol to easy to use Go types. It is meant as a single component in a larger IRC library, or for basic IRC bots for which a large IRC package would be overkill.* ### Message The [Message][] and [Prefix][] types provide translation to and from IRC message format. // Parse the IRC-encoded data and stores the result in a new struct. message := irc.ParseMessage(raw) // Returns the IRC encoding of the message. raw = message.String() ### Encoder & Decoder The [Encoder][] and [Decoder][] types allow working with IRC message streams. // Create a decoder that reads from given io.Reader dec := irc.NewDecoder(reader) // Decode the next IRC message message, err := dec.Decode() // Create an encoder that writes to given io.Writer enc := irc.NewEncoder(writer) // Send a message to the writer. enc.Encode(message) ### Conn The [Conn][] type combines an [Encoder][] and [Decoder][] for a duplex connection. c, err := irc.Dial("irc.server.net:6667") // Methods from both Encoder and Decoder are available message, err := c.Decode() ## Examples Check these other projects for an example on how to use the package: Clients: - https://github.com/nickvanw/ircx (great simple example) - https://github.com/FSX/jun - https://github.com/jnwhiteh/wallops - https://github.com/Alligator/gomero - https://github.com/msparks/iq - https://github.com/TheCreeper/HackBot Servers: - https://github.com/nightexcessive/excessiveircd [Documentation]: https://godoc.org/github.com/sorcix/irc "Package documentation by Godoc.org" [Message]: https://godoc.org/github.com/sorcix/irc#Message "Message type documentation" [Prefix]: https://godoc.org/github.com/sorcix/irc#Prefix "Prefix type documentation" [Encoder]: https://godoc.org/github.com/sorcix/irc#Encoder "Encoder type documentation" [Decoder]: https://godoc.org/github.com/sorcix/irc#Decoder "Decoder type documentation" [Conn]: https://godoc.org/github.com/sorcix/irc#Conn "Conn type documentation" [RFC1459]: https://tools.ietf.org/html/rfc1459.html "RFC 1459" irc-1.1.0/constants.go000066400000000000000000000166141246415563400146530ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. package irc // Various prefixes extracted from RFC1459. const ( Channel = '#' // Normal channel Distributed = '&' // Distributed channel Owner = '~' // Channel owner +q (non-standard) Admin = '&' // Channel admin +a (non-standard) Operator = '@' // Channel operator +o HalfOperator = '%' // Channel half operator +h (non-standard) Voice = '+' // User has voice +v ) // User modes as defined by RFC1459 section 4.2.3.2. const ( UserModeInvisible = 'i' // User is invisible UserModeServerNotices = 's' // User wants to receive server notices UserModeWallops = 'w' // User wants to receive Wallops UserModeOperator = 'o' // Server operator ) // Channel modes as defined by RFC1459 section 4.2.3.1 const ( ModeOperator = 'o' // Operator privileges ModeVoice = 'v' // Ability to speak on a moderated channel ModePrivate = 'p' // Private channel ModeSecret = 's' // Secret channel ModeInviteOnly = 'i' // Users can't join without invite ModeTopic = 't' // Topic can only be set by an operator ModeModerated = 'm' // Only voiced users and operators can talk ModeLimit = 'l' // User limit ModeKey = 'k' // Channel password ModeOwner = 'q' // Owner privileges (non-standard) ModeAdmin = 'a' // Admin privileges (non-standard) ModeHalfOperator = 'h' // Half-operator privileges (non-standard) ) // IRC commands extracted from RFC2812 section 3 and RFC2813 section 4. const ( PASS = "PASS" NICK = "NICK" USER = "USER" OPER = "OPER" MODE = "MODE" SERVICE = "SERVICE" QUIT = "QUIT" SQUIT = "SQUIT" JOIN = "JOIN" PART = "PART" TOPIC = "TOPIC" NAMES = "NAMES" LIST = "LIST" INVITE = "INVITE" KICK = "KICK" PRIVMSG = "PRIVMSG" NOTICE = "NOTICE" MOTD = "MOTD" LUSERS = "LUSERS" VERSION = "VERSION" STATS = "STATS" LINKS = "LINKS" TIME = "TIME" CONNECT = "CONNECT" TRACE = "TRACE" ADMIN = "ADMIN" INFO = "INFO" SERVLIST = "SERVLIST" SQUERY = "SQUERY" WHO = "WHO" WHOIS = "WHOIS" WHOWAS = "WHOWAS" KILL = "KILL" PING = "PING" PONG = "PONG" ERROR = "ERROR" AWAY = "AWAY" REHASH = "REHASH" DIE = "DIE" RESTART = "RESTART" SUMMON = "SUMMON" USERS = "USERS" WALLOPS = "WALLOPS" USERHOST = "USERHOST" ISON = "ISON" SERVER = "SERVER" NJOIN = "NJOIN" ) // Numeric IRC replies extracted from RFC2812 section 5. const ( RPL_WELCOME = "001" RPL_YOURHOST = "002" RPL_CREATED = "003" RPL_MYINFO = "004" RPL_BOUNCE = "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" ) // IRC commands extracted from the IRCv3 spec at http://www.ircv3.org/. const ( CAP = "CAP" CAP_LS = "LS" // Subcommand (param) CAP_LIST = "LIST" // Subcommand (param) CAP_REQ = "REQ" // Subcommand (param) CAP_ACK = "ACK" // Subcommand (param) CAP_NAK = "NAK" // Subcommand (param) CAP_CLEAR = "CLEAR" // Subcommand (param) CAP_END = "END" // Subcommand (param) AUTHENTICATE = "AUTHENTICATE" ) // Numeric IRC replies extracted from the IRCv3 spec. 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" ) irc-1.1.0/ctcp/000077500000000000000000000000001246415563400132315ustar00rootroot00000000000000irc-1.1.0/ctcp/ctcp.go000066400000000000000000000067511246415563400145220ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. package ctcp // Sources: // http://www.irchelp.org/irchelp/rfc/ctcpspec.html // http://www.kvirc.net/doc/doc_ctcp_handling.html import ( "fmt" "runtime" "strings" "time" ) // Various constants used for formatting CTCP messages. const ( delimiter byte = 0x01 // Prefix and suffix for CTCP tagged messages. space byte = 0x20 // Token separator empty = "" // The empty string timeFormat = time.RFC1123Z versionFormat = "Go v%s (" + runtime.GOOS + ", " + runtime.GOARCH + ")" ) // Tags extracted from the CTCP spec. const ( ACTION = "ACTION" PING = "PING" PONG = "PONG" VERSION = "VERSION" USERINFO = "USERINFO" CLIENTINFO = "CLIENTINFO" FINGER = "FINGER" SOURCE = "SOURCE" TIME = "TIME" ) // Decode attempts to decode CTCP tagged data inside given message text. // // If the message text does not contain tagged data, ok will be false. // // ::= [ ] // ::= 0x01 // func Decode(text string) (tag, message string, ok bool) { // Fast path, return if this text does not contain a CTCP message. if len(text) < 3 || text[0] != delimiter || text[len(text)-1] != delimiter { return empty, empty, false } s := strings.IndexByte(text, space) if s < 0 { // Messages may contain only a tag. return text[1 : len(text)-1], empty, true } return text[1:s], text[s+1 : len(text)-1], true } // Encode returns the IRC message text for CTCP tagged data. // // ::= [ ] // ::= 0x01 // func Encode(tag, message string) (text string) { switch { // We can't build a valid CTCP tagged message without at least a tag. case len(tag) <= 0: return empty // Tagged data with a message case len(message) > 0: return string(delimiter) + tag + string(space) + message + string(delimiter) // Tagged data without a message default: return string(delimiter) + tag + string(delimiter) } } // Action is a shortcut for Encode(ctcp.ACTION, message). func Action(message string) string { return Encode(ACTION, message) } // Ping is a shortcut for Encode(ctcp.PING, message). func Ping(message string) string { return Encode(PING, message) } // Pong is a shortcut for Encode(ctcp.PONG, message). func Pong(message string) string { return Encode(PONG, message) } // Version is a shortcut for Encode(ctcp.VERSION, message). func Version(message string) string { return Encode(VERSION, message) } // VersionReply is a shortcut for ENCODE(ctcp.VERSION, go version info). func VersionReply() string { return Encode(VERSION, fmt.Sprintf(versionFormat, runtime.Version())) } // UserInfo is a shortcut for Encode(ctcp.USERINFO, message). func UserInfo(message string) string { return Encode(USERINFO, message) } // ClientInfo is a shortcut for Encode(ctcp.CLIENTINFO, message). func ClientInfo(message string) string { return Encode(CLIENTINFO, message) } // Finger is a shortcut for Encode(ctcp.FINGER, message). func Finger(message string) string { return Encode(FINGER, message) } // Source is a shortcut for Encode(ctcp.SOURCE, message). func Source(message string) string { return Encode(SOURCE, message) } // Time is a shortcut for Encode(ctcp.TIME, message). func Time(message string) string { return Encode(TIME, message) } // TimeReply is a shortcut for Encode(ctcp.TIME, currenttime). func TimeReply() string { return Encode(TIME, time.Now().Format(timeFormat)) } irc-1.1.0/ctcp/ctcp_test.go000066400000000000000000000052011246415563400155460ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. package ctcp import ( "testing" ) func TestDecode(t *testing.T) { if _, _, ok := Decode("\x01\x01"); ok { t.Error("Message is invalid, but ok is true.") } if _, _, ok := Decode("\x01"); ok { t.Error("Message is invalid, but ok is true.") } if _, _, ok := Decode("\x01VERSION"); ok { t.Error("Message is invalid, but ok is true.") } if tag, message, ok := Decode("\x01VERSION\x01"); tag != "VERSION" || len(message) > 0 || !ok { t.Error("Message contains only a tag, wrong results.") } if tag, message, ok := Decode("\x01PING 123456789\x01"); tag != "PING" || message != "123456789" || !ok { t.Error("Message contains tag and a message, wrong results.") } if tag, message, ok := Decode("\x01CLIENTINFO A B C\x01"); tag != "CLIENTINFO" || message != "A B C" || !ok { t.Error("Message contains tag and a message with spaces, wrong results.") } } func TestEncode(t *testing.T) { if text := Encode("", "INVALID"); len(text) > 0 { t.Error("Message is invalid, but returns a non-empty string.") } if text := Encode("VERSION", ""); text != "\x01VERSION\x01" { t.Error("Message contains only a tag, wrong results.") } if text := Encode("PING", "123456789"); text != "\x01PING 123456789\x01" { t.Error("Message contains tag and a message, wrong results.") } if text := Encode("CLIENTINFO", "A B C"); text != "\x01CLIENTINFO A B C\x01" { t.Error("Message contains tag and a message with spaces, wrong results.") } } func TestAction(t *testing.T) { if text := Action("A B C"); text != "\x01ACTION A B C\x01" { t.Error("Wrong result!") } } func TestPing(t *testing.T) { if text := Ping("A B C"); text != "\x01PING A B C\x01" { t.Error("Wrong result!") } } func TestPong(t *testing.T) { if text := Pong("A B C"); text != "\x01PONG A B C\x01" { t.Error("Wrong result!") } } func TestVersion(t *testing.T) { if text := Version("A B C"); text != "\x01VERSION A B C\x01" { t.Error("Wrong result!") } } func TestUserInfo(t *testing.T) { if text := UserInfo("A B C"); text != "\x01USERINFO A B C\x01" { t.Error("Wrong result!") } } func TestClientInfo(t *testing.T) { if text := ClientInfo("A B C"); text != "\x01CLIENTINFO A B C\x01" { t.Error("Wrong result!") } } func TestFinger(t *testing.T) { if text := Finger("A B C"); text != "\x01FINGER A B C\x01" { t.Error("Wrong result!") } } func TestSource(t *testing.T) { if text := Source("A B C"); text != "\x01SOURCE A B C\x01" { t.Error("Wrong result!") } } func TestTime(t *testing.T) { if text := Time("A B C"); text != "\x01TIME A B C\x01" { t.Error("Wrong result!") } } irc-1.1.0/ctcp/doc.go000066400000000000000000000020671246415563400143320ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. // Package ctcp implements partial support for the Client-to-Client Protocol. // // CTCP defines extended messages using the standard PRIVMSG and NOTICE // commands in IRC. This means that any CTCP messages are embedded inside the // normal message text. Clients that don't support CTCP simply show // the encoded message to the user. // // Most IRC clients support only a subset of the protocol, and only a few // commands are actually used. This package aims to implement the most basic // CTCP messages: a single command per IRC message. Quoting is not supported. // // Example using the irc.Message type: // // m := irc.ParseMessage(...) // // if tag, text, ok := ctcp.Decode(m.Trailing); ok { // // This is a CTCP message. // } else { // // This is not a CTCP message. // } // // Similar, for encoding messages: // // m.Trailing = ctcp.Encode("ACTION","wants a cookie!") // // Do not send a complete IRC message to Decode, it won't work. package ctcp irc-1.1.0/doc.go000066400000000000000000000021641246415563400133770ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. // Package irc allows your application to speak the IRC protocol. // // The Message and Prefix structs provide translation to and from raw IRC messages: // // // Parse the IRC-encoded data and store the result in a new struct: // message := irc.ParseMessage(raw) // // // Translate back to a raw IRC message string: // raw = message.String() // // Decoder and Encoder can be used to decode and encode messages in a stream: // // // Create a decoder that reads from given io.Reader // dec := irc.NewDecoder(reader) // // // Decode the next IRC message // message, err := dec.Decode() // // // Create an encoder that writes to given io.Writer // enc := irc.NewEncoder(writer) // // // Send a message to the writer. // enc.Encode(message) // // The Conn type combines an Encoder and Decoder for a duplex connection. // // c, err := irc.Dial("irc.server.net:6667") // // // Methods from both Encoder and Decoder are available // message, err := c.Decode() // package irc // import "github.com/sorcix/irc" irc-1.1.0/message.go000066400000000000000000000161631246415563400142620ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. package irc import ( "bytes" "strings" ) // Various constants used for formatting IRC messages. 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 512 - 2 for the line endings. ) func cutsetFunc(r rune) bool { // Characters to trim from prefixes/messages. return r == '\r' || r == '\n' || r == '\x20' || r == '\x00' } // Objects implementing the Sender interface are able to send messages to an IRC server. // // As there might be a message queue, it is possible that Send returns a nil // error, but the message is not sent (yet). The error value is only used when // it is certain that sending the message is impossible. // // This interface is not used inside this package, and shouldn't have been // defined here in the first place. For backwards compatibility only. type Sender interface { Send(*Message) error } // Prefix represents the prefix (sender) of an IRC message. // See RFC1459 section 2.3.1. // // | [ '!' ] [ '@' ] // 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 this prefix. func (p *Prefix) Len() (length int) { length = len(p.Name) if len(p.User) > 0 { length = length + len(p.User) + 1 } if len(p.Host) > 0 { length = length + len(p.Host) + 1 } return } // Bytes returns a []byte representation of this prefix. func (p *Prefix) Bytes() []byte { buffer := new(bytes.Buffer) p.writeTo(buffer) return buffer.Bytes() } // String returns a string representation of this prefix. func (p *Prefix) String() (s string) { // Benchmarks revealed that in this case simple string concatenation // is actually faster than using a ByteBuffer as in (*Message).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 this 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 Message.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 } // Message represents an IRC protocol message. // See RFC1459 section 2.3.1. // // ::= [':' ] // ::= | [ '!' ] [ '@' ] // ::= { } | // ::= ' ' { ' ' } // ::= [ ':' | ] // // ::= // ::= // // ::= CR LF type Message 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 } // ParseMessage takes a string and attempts to create a Message struct. // Returns nil if the Message is invalid. func ParseMessage(raw string) (m *Message) { // Ignore empty messages. if raw = strings.TrimFunc(raw, cutsetFunc); len(raw) < 2 { return nil } i, j := 0, 0 m = new(Message) 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 } m.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 { m.Command = raw[i:j] } else { m.Command = raw[i:] // We're done here! return m } // Skip space after command j++ // Find prefix for trailer i = indexByte(raw[j:], prefix) if i < 0 { // There is no trailing argument! m.Params = strings.Split(raw[j:], string(space)) // We're done here! return m } // Compensate for index on substring i = i + j // Check if we need to parse arguments. if i > j { m.Params = strings.Split(raw[j:i-1], string(space)) } m.Trailing = raw[i+1:] // We need to re-encode the trailing argument even if it was empty. if len(m.Trailing) <= 0 { m.EmptyTrailing = true } return m } // Len calculates the length of the string representation of this message. func (m *Message) Len() (length int) { if m.Prefix != nil { length = m.Prefix.Len() + 2 // Include prefix and trailing space } length = length + len(m.Command) if len(m.Params) > 0 { length = length + len(m.Params) for _, param := range m.Params { length = length + len(param) } } if len(m.Trailing) > 0 || m.EmptyTrailing { length = length + len(m.Trailing) + 2 // Include prefix and space } return } // Bytes returns a []byte representation of this message. // // 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 (m *Message) Bytes() []byte { buffer := new(bytes.Buffer) // Message prefix if m.Prefix != nil { buffer.WriteByte(prefix) m.Prefix.writeTo(buffer) buffer.WriteByte(space) } // Command is required buffer.WriteString(m.Command) // Space separated list of arguments if len(m.Params) > 0 { buffer.WriteByte(space) buffer.WriteString(strings.Join(m.Params, string(space))) } if len(m.Trailing) > 0 || m.EmptyTrailing { buffer.WriteByte(space) buffer.WriteByte(prefix) buffer.WriteString(m.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 message. // // 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 (m *Message) String() string { return string(m.Bytes()) } irc-1.1.0/message_test.go000066400000000000000000000252541246415563400153220ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. package irc import ( "reflect" "testing" ) var messageTests = [...]*struct { parsed *Message rawMessage string rawPrefix string hostmask bool // Is it very clear that the prefix is a hostname? server bool // Is the prefix a servername? }{ { parsed: &Message{ Prefix: &Prefix{ Name: "syrk", User: "kalt", Host: "millennium.stealth.net", }, Command: "QUIT", Trailing: "Gone to have lunch", }, rawMessage: ":syrk!kalt@millennium.stealth.net QUIT :Gone to have lunch", rawPrefix: "syrk!kalt@millennium.stealth.net", hostmask: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "Trillian", }, Command: "SQUIT", Params: []string{"cm22.eng.umd.edu"}, Trailing: "Server out of control", }, rawMessage: ":Trillian SQUIT cm22.eng.umd.edu :Server out of control", rawPrefix: "Trillian", server: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "WiZ", User: "jto", Host: "tolsun.oulu.fi", }, Command: "JOIN", Params: []string{"#Twilight_zone"}, }, rawMessage: ":WiZ!jto@tolsun.oulu.fi JOIN #Twilight_zone", rawPrefix: "WiZ!jto@tolsun.oulu.fi", hostmask: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "WiZ", User: "jto", Host: "tolsun.oulu.fi", }, Command: "PART", Params: []string{"#playzone"}, Trailing: "I lost", }, rawMessage: ":WiZ!jto@tolsun.oulu.fi PART #playzone :I lost", rawPrefix: "WiZ!jto@tolsun.oulu.fi", hostmask: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "WiZ", User: "jto", Host: "tolsun.oulu.fi", }, Command: "MODE", Params: []string{"#eu-opers", "-l"}, }, rawMessage: ":WiZ!jto@tolsun.oulu.fi MODE #eu-opers -l", rawPrefix: "WiZ!jto@tolsun.oulu.fi", hostmask: true, }, { parsed: &Message{ Command: "MODE", Params: []string{"&oulu", "+b", "*!*@*.edu", "+e", "*!*@*.bu.edu"}, }, rawMessage: "MODE &oulu +b *!*@*.edu +e *!*@*.bu.edu", }, { parsed: &Message{ Command: "PRIVMSG", Params: []string{"#channel"}, Trailing: "Message with :colons!", }, rawMessage: "PRIVMSG #channel :Message with :colons!", }, { parsed: &Message{ Prefix: &Prefix{ Name: "irc.vives.lan", }, Command: "251", Params: []string{"test"}, Trailing: "There are 2 users and 0 services on 1 servers", }, rawMessage: ":irc.vives.lan 251 test :There are 2 users and 0 services on 1 servers", rawPrefix: "irc.vives.lan", server: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "irc.vives.lan", }, Command: "376", Params: []string{"test"}, Trailing: "End of MOTD command", }, rawMessage: ":irc.vives.lan 376 test :End of MOTD command", rawPrefix: "irc.vives.lan", server: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "irc.vives.lan", }, Command: "250", Params: []string{"test"}, Trailing: "Highest connection count: 1 (1 connections received)", }, rawMessage: ":irc.vives.lan 250 test :Highest connection count: 1 (1 connections received)", rawPrefix: "irc.vives.lan", server: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "sorcix", User: "~sorcix", Host: "sorcix.users.quakenet.org", }, Command: "PRIVMSG", Params: []string{"#viveslan"}, Trailing: "\001ACTION is testing CTCP messages!\001", }, rawMessage: ":sorcix!~sorcix@sorcix.users.quakenet.org PRIVMSG #viveslan :\001ACTION is testing CTCP messages!\001", rawPrefix: "sorcix!~sorcix@sorcix.users.quakenet.org", hostmask: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "sorcix", User: "~sorcix", Host: "sorcix.users.quakenet.org", }, Command: "NOTICE", Params: []string{"midnightfox"}, Trailing: "\001PONG 1234567890\001", }, rawMessage: ":sorcix!~sorcix@sorcix.users.quakenet.org NOTICE midnightfox :\001PONG 1234567890\001", rawPrefix: "sorcix!~sorcix@sorcix.users.quakenet.org", hostmask: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "a", User: "b", Host: "c", }, Command: "QUIT", }, rawMessage: ":a!b@c QUIT", rawPrefix: "a!b@c", hostmask: true, }, { parsed: &Message{ Prefix: &Prefix{ Name: "a", User: "b", }, Command: "PRIVMSG", Trailing: "message", }, rawMessage: ":a!b PRIVMSG :message", rawPrefix: "a!b", }, { parsed: &Message{ Prefix: &Prefix{ Name: "a", Host: "c", }, Command: "NOTICE", Trailing: ":::Hey!", }, rawMessage: ":a@c NOTICE ::::Hey!", rawPrefix: "a@c", }, { parsed: &Message{ Prefix: &Prefix{ Name: "nick", }, Command: "PRIVMSG", Params: []string{"$@"}, Trailing: "This message contains a\ttab!", }, rawMessage: ":nick PRIVMSG $@ :This message contains a\ttab!", rawPrefix: "nick", }, { parsed: &Message{ Command: "TEST", Params: []string{"$@", "", "param"}, Trailing: "Trailing", }, rawMessage: "TEST $@ param :Trailing", }, { rawMessage: ": PRIVMSG test :Invalid message with empty prefix.", rawPrefix: "", }, { rawMessage: ": PRIVMSG test :Invalid message with space prefix", rawPrefix: " ", }, { parsed: &Message{ Command: "TOPIC", Params: []string{"#foo"}, Trailing: "", }, rawMessage: "TOPIC #foo", rawPrefix: "", }, { parsed: &Message{ Command: "TOPIC", Params: []string{"#foo"}, Trailing: "", EmptyTrailing: true, }, rawMessage: "TOPIC #foo :", rawPrefix: "", }, } // ----- // PREFIX // ----- func TestPrefix_IsHostmask(t *testing.T) { for i, test := range messageTests { // Skip tests that have no prefix if test.parsed == nil || test.parsed.Prefix == nil { continue } if test.hostmask && !test.parsed.Prefix.IsHostmask() { t.Errorf("Prefix %d should be recognized as a hostmask!", i) } } } func TestPrefix_IsServer(t *testing.T) { for i, test := range messageTests { // Skip tests that have no prefix if test.parsed == nil || test.parsed.Prefix == nil { continue } if test.server && !test.parsed.Prefix.IsServer() { t.Errorf("Prefix %d should be recognized as a server!", i) } } } func TestPrefix_String(t *testing.T) { var s string for i, test := range messageTests { // Skip tests that have no prefix if test.parsed == nil || test.parsed.Prefix == nil { continue } // Convert the prefix s = test.parsed.Prefix.String() // Result should be the same as the value in rawMessage. if s != test.rawPrefix { t.Errorf("Failed to stringify prefix %d:", i) t.Logf("Output: %s", s) t.Logf("Expected: %s", test.rawPrefix) } } } func TestPrefix_Len(t *testing.T) { var l int for i, test := range messageTests { // Skip tests that have no prefix if test.parsed == nil || test.parsed.Prefix == nil { continue } l = test.parsed.Prefix.Len() // Result should be the same as the value in rawMessage. if l != len(test.rawPrefix) { t.Errorf("Failed to calculate prefix length %d:", i) t.Logf("Output: %d", l) t.Logf("Expected: %d", len(test.rawPrefix)) } } } func TestParsePrefix(t *testing.T) { var p *Prefix for i, test := range messageTests { // Skip tests that have no prefix if test.parsed == nil || test.parsed.Prefix == nil { continue } // Parse the prefix p = ParsePrefix(test.rawPrefix) // Result struct should be the same as the value in parsed. if *p != *test.parsed.Prefix { t.Errorf("Failed to parse prefix %d:", i) t.Logf("Output: %#v", p) t.Logf("Expected: %#v", test.parsed.Prefix) } } } // ----- // MESSAGE // ----- func TestMessage_String(t *testing.T) { var s string for i, test := range messageTests { // Skip tests that have no valid struct if test.parsed == nil { continue } // Convert the prefix s = test.parsed.String() // Result should be the same as the value in rawMessage. if s != test.rawMessage { t.Errorf("Failed to stringify message %d:", i) t.Logf("Output: %s", s) t.Logf("Expected: %s", test.rawMessage) } } } func TestMessage_Len(t *testing.T) { var l int for i, test := range messageTests { // Skip tests that have no valid struct if test.parsed == nil { continue } l = test.parsed.Len() // Result should be the same as the value in rawMessage. if l != len(test.rawMessage) { t.Errorf("Failed to calculate message length %d:", i) t.Logf("Output: %d", l) t.Logf("Expected: %d", len(test.rawMessage)) } } } func TestParseMessage(t *testing.T) { var p *Message for i, test := range messageTests { // Parse the prefix p = ParseMessage(test.rawMessage) // Result struct should be the same as the value in parsed. if !reflect.DeepEqual(p, test.parsed) { t.Errorf("Failed to parse message %d:", i) t.Logf("Output: %#v", p) t.Logf("Expected: %#v", test.parsed) } } } // ----- // MESSAGE DECODE -> ENCODE // ----- func TestMessageDecodeEncode(t *testing.T) { var ( p *Message s string ) for i, test := range messageTests { // Skip invalid messages if test.parsed == nil { continue } // Decode the message, then encode it again. p = ParseMessage(test.rawMessage) s = p.String() // Result struct should be the same as the original. if s != test.rawMessage { t.Errorf("Message %d failed decode-encode sequence!", i) } } } // ----- // BENCHMARK // ----- func BenchmarkPrefix_String_short(b *testing.B) { b.ReportAllocs() prefix := new(Prefix) prefix.Name = "Namename" b.ResetTimer() for i := 0; i < b.N; i++ { prefix.String() } } func BenchmarkPrefix_String_long(b *testing.B) { b.ReportAllocs() prefix := new(Prefix) prefix.Name = "Namename" prefix.User = "Username" prefix.Host = "Hostname" b.ResetTimer() for i := 0; i < b.N; i++ { prefix.String() } } func BenchmarkParsePrefix_short(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { ParsePrefix("Namename") } } func BenchmarkParsePrefix_long(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { ParsePrefix("Namename!Username@Hostname") } } func BenchmarkMessage_String(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { messageTests[0].parsed.String() } } func BenchmarkParseMessage_short(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { ParseMessage("COMMAND arg1 :Message\r\n") } } func BenchmarkParseMessage_medium(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { ParseMessage(":Namename COMMAND arg6 arg7 :Message message message\r\n") } } func BenchmarkParseMessage_long(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { ParseMessage(":Namename!username@hostname COMMAND arg1 arg2 arg3 arg4 arg5 arg6 arg7 :Message message message message message\r\n") } } irc-1.1.0/stream.go000066400000000000000000000052531246415563400141270ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. package irc 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 message parsing. const delim byte = '\n' var endline = []byte("\r\n") // A 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 Message 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 Message from the stream. // // Returns a non-nil error if the read failed. func (dec *Decoder) Decode() (m *Message, err error) { dec.mu.Lock() dec.line, err = dec.reader.ReadString(delim) dec.mu.Unlock() if err != nil { return nil, err } return ParseMessage(dec.line), nil } // An Encoder writes Message 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. // // This method may be used from multiple goroutines. // // Returns an non-nil error if the write to the underlying stream stopped early. func (enc *Encoder) Encode(m *Message) (err error) { _, err = enc.Write(m.Bytes()) return } // Write writes len(p) bytes from p followed by CR+LF. // // This method can be used simultaneously from multiple goroutines, // it guarantees to serialize access. However, writing a single IRC message // using multiple Write calls will cause corruption. 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 } irc-1.1.0/stream_test.go000066400000000000000000000031711246415563400151630ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. package irc import ( "bytes" "io" "reflect" "strings" "testing" ) var stream = "PING port80a.se.quakenet.org\r\n:port80a.se.quakenet.org PONG port80a.se.quakenet.org :port80a.se.quakenet.org\r\nPING chat.freenode.net\r\n:wilhelm.freenode.net PONG wilhelm.freenode.net :chat.freenode.net\r\n" var result = [...]*Message{ { Command: PING, Params: []string{"port80a.se.quakenet.org"}, }, { Prefix: &Prefix{ Name: "port80a.se.quakenet.org", }, Command: PONG, Params: []string{"port80a.se.quakenet.org"}, Trailing: "port80a.se.quakenet.org", }, { Command: PING, Params: []string{"chat.freenode.net"}, }, { Prefix: &Prefix{ Name: "wilhelm.freenode.net", }, Command: PONG, Params: []string{"wilhelm.freenode.net"}, Trailing: "chat.freenode.net", }, } func TestDecoder_Decode(t *testing.T) { reader := strings.NewReader(stream) dec := NewDecoder(reader) for i, test := range result { if message, err := dec.Decode(); err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } else { if !reflect.DeepEqual(message, test) { t.Fatalf("Decoded message looks wrong! (%d)", i) } } } if _, err := dec.Decode(); err != io.EOF { t.Fatal("Decode should return an EOF error!") } } func TestEncoder_Encode(t *testing.T) { buffer := new(bytes.Buffer) enc := NewEncoder(buffer) for _, test := range result { if err := enc.Encode(test); err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } } if buffer.String() != stream { t.Fatalf("Encoded stream looks wrong!") } } irc-1.1.0/strings.go000066400000000000000000000004001246415563400143120ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. // +build go1.2 // Documented in strings_legacy.go package irc import ( "strings" ) func indexByte(s string, c byte) int { return strings.IndexByte(s, c) } irc-1.1.0/strings_legacy.go000066400000000000000000000006741246415563400156530ustar00rootroot00000000000000// Copyright 2014 Vic Demuzere // // Use of this source code is governed by the MIT license. // +build !go1.2 // Debian Wheezy only ships Go 1.0: // https://github.com/sorcix/irc/issues/4 // // This code may be removed when Wheezy is no longer supported. package irc // indexByte implements strings.IndexByte for Go versions < 1.2. func indexByte(s string, c byte) int { for i := range s { if s[i] == c { return i } } return -1 }