├── .travis.yml ├── LICENSE ├── README.md ├── constants.go ├── ctcp ├── ctcp.go ├── ctcp_test.go └── doc.go ├── doc.go ├── go.mod ├── message.go ├── message_test.go ├── stream.go ├── stream_test.go ├── strings.go └── strings_legacy.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: gopkg.in/sorcix/irc.v1 3 | go: 4 | - 1.0 5 | - 1.1 6 | - 1.2 7 | - 1.3 8 | - 1.4 9 | - 1.5 10 | - 1.6 11 | - tip 12 | script: 13 | - go test -v -bench=. 14 | matrix: 15 | allow_failures: 16 | - go: 1.0 17 | - go: 1.1 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Vic Demuzere 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go **irc** package 2 | 3 | Please use the [**v2** branch][v2] for new projects! 4 | 5 | [![GoDoc](https://godoc.org/gopkg.in/sorcix/irc.v1?status.svg)](https://godoc.org/gopkg.in/sorcix/irc.v1) 6 | 7 | ## Features 8 | Package irc allows your application to speak the IRC protocol. 9 | 10 | - **Limited scope**, does one thing and does it well. 11 | - Focus on simplicity and **speed**. 12 | - **Stable API**: updates shouldn't break existing software. 13 | - Well [documented][Documentation] code. 14 | 15 | *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.* 16 | 17 | ## Usage 18 | 19 | Please use the [**v2** branch][v2] for new projects! 20 | 21 | [Documentation]: https://godoc.org/gopkg.in/sorcix/irc.v1 "Package documentation by Godoc.org" 22 | [v2]: https://github.com/sorcix/irc/tree/v2 23 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | package irc 6 | 7 | // Various prefixes extracted from RFC1459. 8 | const ( 9 | Channel = '#' // Normal channel 10 | Distributed = '&' // Distributed channel 11 | 12 | Owner = '~' // Channel owner +q (non-standard) 13 | Admin = '&' // Channel admin +a (non-standard) 14 | Operator = '@' // Channel operator +o 15 | HalfOperator = '%' // Channel half operator +h (non-standard) 16 | Voice = '+' // User has voice +v 17 | ) 18 | 19 | // User modes as defined by RFC1459 section 4.2.3.2. 20 | const ( 21 | UserModeInvisible = 'i' // User is invisible 22 | UserModeServerNotices = 's' // User wants to receive server notices 23 | UserModeWallops = 'w' // User wants to receive Wallops 24 | UserModeOperator = 'o' // Server operator 25 | ) 26 | 27 | // Channel modes as defined by RFC1459 section 4.2.3.1 28 | const ( 29 | ModeOperator = 'o' // Operator privileges 30 | ModeVoice = 'v' // Ability to speak on a moderated channel 31 | ModePrivate = 'p' // Private channel 32 | ModeSecret = 's' // Secret channel 33 | ModeInviteOnly = 'i' // Users can't join without invite 34 | ModeTopic = 't' // Topic can only be set by an operator 35 | ModeModerated = 'm' // Only voiced users and operators can talk 36 | ModeLimit = 'l' // User limit 37 | ModeKey = 'k' // Channel password 38 | 39 | ModeOwner = 'q' // Owner privileges (non-standard) 40 | ModeAdmin = 'a' // Admin privileges (non-standard) 41 | ModeHalfOperator = 'h' // Half-operator privileges (non-standard) 42 | ) 43 | 44 | // IRC commands extracted from RFC2812 section 3 and RFC2813 section 4. 45 | const ( 46 | PASS = "PASS" 47 | NICK = "NICK" 48 | USER = "USER" 49 | OPER = "OPER" 50 | MODE = "MODE" 51 | SERVICE = "SERVICE" 52 | QUIT = "QUIT" 53 | SQUIT = "SQUIT" 54 | JOIN = "JOIN" 55 | PART = "PART" 56 | TOPIC = "TOPIC" 57 | NAMES = "NAMES" 58 | LIST = "LIST" 59 | INVITE = "INVITE" 60 | KICK = "KICK" 61 | PRIVMSG = "PRIVMSG" 62 | NOTICE = "NOTICE" 63 | MOTD = "MOTD" 64 | LUSERS = "LUSERS" 65 | VERSION = "VERSION" 66 | STATS = "STATS" 67 | LINKS = "LINKS" 68 | TIME = "TIME" 69 | CONNECT = "CONNECT" 70 | TRACE = "TRACE" 71 | ADMIN = "ADMIN" 72 | INFO = "INFO" 73 | SERVLIST = "SERVLIST" 74 | SQUERY = "SQUERY" 75 | WHO = "WHO" 76 | WHOIS = "WHOIS" 77 | WHOWAS = "WHOWAS" 78 | KILL = "KILL" 79 | PING = "PING" 80 | PONG = "PONG" 81 | ERROR = "ERROR" 82 | AWAY = "AWAY" 83 | REHASH = "REHASH" 84 | DIE = "DIE" 85 | RESTART = "RESTART" 86 | SUMMON = "SUMMON" 87 | USERS = "USERS" 88 | WALLOPS = "WALLOPS" 89 | USERHOST = "USERHOST" 90 | ISON = "ISON" 91 | SERVER = "SERVER" 92 | NJOIN = "NJOIN" 93 | ) 94 | 95 | // Numeric IRC replies extracted from RFC2812 section 5. 96 | const ( 97 | RPL_WELCOME = "001" 98 | RPL_YOURHOST = "002" 99 | RPL_CREATED = "003" 100 | RPL_MYINFO = "004" 101 | RPL_BOUNCE = "005" 102 | RPL_ISUPPORT = "005" 103 | RPL_USERHOST = "302" 104 | RPL_ISON = "303" 105 | RPL_AWAY = "301" 106 | RPL_UNAWAY = "305" 107 | RPL_NOWAWAY = "306" 108 | RPL_WHOISUSER = "311" 109 | RPL_WHOISSERVER = "312" 110 | RPL_WHOISOPERATOR = "313" 111 | RPL_WHOISIDLE = "317" 112 | RPL_ENDOFWHOIS = "318" 113 | RPL_WHOISCHANNELS = "319" 114 | RPL_WHOWASUSER = "314" 115 | RPL_ENDOFWHOWAS = "369" 116 | RPL_LISTSTART = "321" 117 | RPL_LIST = "322" 118 | RPL_LISTEND = "323" 119 | RPL_UNIQOPIS = "325" 120 | RPL_CHANNELMODEIS = "324" 121 | RPL_NOTOPIC = "331" 122 | RPL_TOPIC = "332" 123 | RPL_INVITING = "341" 124 | RPL_SUMMONING = "342" 125 | RPL_INVITELIST = "346" 126 | RPL_ENDOFINVITELIST = "347" 127 | RPL_EXCEPTLIST = "348" 128 | RPL_ENDOFEXCEPTLIST = "349" 129 | RPL_VERSION = "351" 130 | RPL_WHOREPLY = "352" 131 | RPL_ENDOFWHO = "315" 132 | RPL_NAMREPLY = "353" 133 | RPL_ENDOFNAMES = "366" 134 | RPL_LINKS = "364" 135 | RPL_ENDOFLINKS = "365" 136 | RPL_BANLIST = "367" 137 | RPL_ENDOFBANLIST = "368" 138 | RPL_INFO = "371" 139 | RPL_ENDOFINFO = "374" 140 | RPL_MOTDSTART = "375" 141 | RPL_MOTD = "372" 142 | RPL_ENDOFMOTD = "376" 143 | RPL_YOUREOPER = "381" 144 | RPL_REHASHING = "382" 145 | RPL_YOURESERVICE = "383" 146 | RPL_TIME = "391" 147 | RPL_USERSSTART = "392" 148 | RPL_USERS = "393" 149 | RPL_ENDOFUSERS = "394" 150 | RPL_NOUSERS = "395" 151 | RPL_TRACELINK = "200" 152 | RPL_TRACECONNECTING = "201" 153 | RPL_TRACEHANDSHAKE = "202" 154 | RPL_TRACEUNKNOWN = "203" 155 | RPL_TRACEOPERATOR = "204" 156 | RPL_TRACEUSER = "205" 157 | RPL_TRACESERVER = "206" 158 | RPL_TRACESERVICE = "207" 159 | RPL_TRACENEWTYPE = "208" 160 | RPL_TRACECLASS = "209" 161 | RPL_TRACERECONNECT = "210" 162 | RPL_TRACELOG = "261" 163 | RPL_TRACEEND = "262" 164 | RPL_STATSLINKINFO = "211" 165 | RPL_STATSCOMMANDS = "212" 166 | RPL_ENDOFSTATS = "219" 167 | RPL_STATSUPTIME = "242" 168 | RPL_STATSOLINE = "243" 169 | RPL_UMODEIS = "221" 170 | RPL_SERVLIST = "234" 171 | RPL_SERVLISTEND = "235" 172 | RPL_LUSERCLIENT = "251" 173 | RPL_LUSEROP = "252" 174 | RPL_LUSERUNKNOWN = "253" 175 | RPL_LUSERCHANNELS = "254" 176 | RPL_LUSERME = "255" 177 | RPL_ADMINME = "256" 178 | RPL_ADMINLOC1 = "257" 179 | RPL_ADMINLOC2 = "258" 180 | RPL_ADMINEMAIL = "259" 181 | RPL_TRYAGAIN = "263" 182 | ERR_NOSUCHNICK = "401" 183 | ERR_NOSUCHSERVER = "402" 184 | ERR_NOSUCHCHANNEL = "403" 185 | ERR_CANNOTSENDTOCHAN = "404" 186 | ERR_TOOMANYCHANNELS = "405" 187 | ERR_WASNOSUCHNICK = "406" 188 | ERR_TOOMANYTARGETS = "407" 189 | ERR_NOSUCHSERVICE = "408" 190 | ERR_NOORIGIN = "409" 191 | ERR_NORECIPIENT = "411" 192 | ERR_NOTEXTTOSEND = "412" 193 | ERR_NOTOPLEVEL = "413" 194 | ERR_WILDTOPLEVEL = "414" 195 | ERR_BADMASK = "415" 196 | ERR_UNKNOWNCOMMAND = "421" 197 | ERR_NOMOTD = "422" 198 | ERR_NOADMININFO = "423" 199 | ERR_FILEERROR = "424" 200 | ERR_NONICKNAMEGIVEN = "431" 201 | ERR_ERRONEUSNICKNAME = "432" 202 | ERR_NICKNAMEINUSE = "433" 203 | ERR_NICKCOLLISION = "436" 204 | ERR_UNAVAILRESOURCE = "437" 205 | ERR_USERNOTINCHANNEL = "441" 206 | ERR_NOTONCHANNEL = "442" 207 | ERR_USERONCHANNEL = "443" 208 | ERR_NOLOGIN = "444" 209 | ERR_SUMMONDISABLED = "445" 210 | ERR_USERSDISABLED = "446" 211 | ERR_NOTREGISTERED = "451" 212 | ERR_NEEDMOREPARAMS = "461" 213 | ERR_ALREADYREGISTRED = "462" 214 | ERR_NOPERMFORHOST = "463" 215 | ERR_PASSWDMISMATCH = "464" 216 | ERR_YOUREBANNEDCREEP = "465" 217 | ERR_YOUWILLBEBANNED = "466" 218 | ERR_KEYSET = "467" 219 | ERR_CHANNELISFULL = "471" 220 | ERR_UNKNOWNMODE = "472" 221 | ERR_INVITEONLYCHAN = "473" 222 | ERR_BANNEDFROMCHAN = "474" 223 | ERR_BADCHANNELKEY = "475" 224 | ERR_BADCHANMASK = "476" 225 | ERR_NOCHANMODES = "477" 226 | ERR_BANLISTFULL = "478" 227 | ERR_NOPRIVILEGES = "481" 228 | ERR_CHANOPRIVSNEEDED = "482" 229 | ERR_CANTKILLSERVER = "483" 230 | ERR_RESTRICTED = "484" 231 | ERR_UNIQOPPRIVSNEEDED = "485" 232 | ERR_NOOPERHOST = "491" 233 | ERR_UMODEUNKNOWNFLAG = "501" 234 | ERR_USERSDONTMATCH = "502" 235 | ) 236 | 237 | // IRC commands extracted from the IRCv3 spec at http://www.ircv3.org/. 238 | const ( 239 | CAP = "CAP" 240 | CAP_LS = "LS" // Subcommand (param) 241 | CAP_LIST = "LIST" // Subcommand (param) 242 | CAP_REQ = "REQ" // Subcommand (param) 243 | CAP_ACK = "ACK" // Subcommand (param) 244 | CAP_NAK = "NAK" // Subcommand (param) 245 | CAP_CLEAR = "CLEAR" // Subcommand (param) 246 | CAP_END = "END" // Subcommand (param) 247 | 248 | AUTHENTICATE = "AUTHENTICATE" 249 | ) 250 | 251 | // Numeric IRC replies extracted from the IRCv3 spec. 252 | const ( 253 | RPL_LOGGEDIN = "900" 254 | RPL_LOGGEDOUT = "901" 255 | RPL_NICKLOCKED = "902" 256 | RPL_SASLSUCCESS = "903" 257 | ERR_SASLFAIL = "904" 258 | ERR_SASLTOOLONG = "905" 259 | ERR_SASLABORTED = "906" 260 | ERR_SASLALREADY = "907" 261 | RPL_SASLMECHS = "908" 262 | ) 263 | 264 | // RFC2812, section 5.3 265 | const ( 266 | RPL_STATSCLINE = "213" 267 | RPL_STATSNLINE = "214" 268 | RPL_STATSILINE = "215" 269 | RPL_STATSKLINE = "216" 270 | RPL_STATSQLINE = "217" 271 | RPL_STATSYLINE = "218" 272 | RPL_SERVICEINFO = "231" 273 | RPL_ENDOFSERVICES = "232" 274 | RPL_SERVICE = "233" 275 | RPL_STATSVLINE = "240" 276 | RPL_STATSLLINE = "241" 277 | RPL_STATSHLINE = "244" 278 | RPL_STATSSLINE = "245" 279 | RPL_STATSPING = "246" 280 | RPL_STATSBLINE = "247" 281 | RPL_STATSDLINE = "250" 282 | RPL_NONE = "300" 283 | RPL_WHOISCHANOP = "316" 284 | RPL_KILLDONE = "361" 285 | RPL_CLOSING = "362" 286 | RPL_CLOSEEND = "363" 287 | RPL_INFOSTART = "373" 288 | RPL_MYPORTIS = "384" 289 | ERR_NOSERVICEHOST = "492" 290 | ) 291 | 292 | // Other constants 293 | const ( 294 | ERR_TOOMANYMATCHES = "416" // Used on IRCNet 295 | RPL_TOPICWHOTIME = "333" // From ircu, in use on Freenode 296 | RPL_LOCALUSERS = "265" // From aircd, Hybrid, Hybrid, Bahamut, in use on Freenode 297 | RPL_GLOBALUSERS = "266" // From aircd, Hybrid, Hybrid, Bahamut, in use on Freenode 298 | ) 299 | -------------------------------------------------------------------------------- /ctcp/ctcp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | package ctcp 6 | 7 | // Sources: 8 | // http://www.irchelp.org/irchelp/rfc/ctcpspec.html 9 | // http://www.kvirc.net/doc/doc_ctcp_handling.html 10 | 11 | import ( 12 | "fmt" 13 | "runtime" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // Various constants used for formatting CTCP messages. 19 | const ( 20 | delimiter byte = 0x01 // Prefix and suffix for CTCP tagged messages. 21 | space byte = 0x20 // Token separator 22 | 23 | empty = "" // The empty string 24 | 25 | timeFormat = time.RFC1123Z 26 | versionFormat = "Go v%s (" + runtime.GOOS + ", " + runtime.GOARCH + ")" 27 | ) 28 | 29 | // Tags extracted from the CTCP spec. 30 | const ( 31 | ACTION = "ACTION" 32 | PING = "PING" 33 | PONG = "PONG" 34 | VERSION = "VERSION" 35 | USERINFO = "USERINFO" 36 | CLIENTINFO = "CLIENTINFO" 37 | FINGER = "FINGER" 38 | SOURCE = "SOURCE" 39 | TIME = "TIME" 40 | ) 41 | 42 | // Decode attempts to decode CTCP tagged data inside given message text. 43 | // 44 | // If the message text does not contain tagged data, ok will be false. 45 | // 46 | // ::= [ ] 47 | // ::= 0x01 48 | // 49 | func Decode(text string) (tag, message string, ok bool) { 50 | 51 | // Fast path, return if this text does not contain a CTCP message. 52 | if len(text) < 3 || text[0] != delimiter || text[len(text)-1] != delimiter { 53 | return empty, empty, false 54 | } 55 | 56 | s := strings.IndexByte(text, space) 57 | 58 | if s < 0 { 59 | 60 | // Messages may contain only a tag. 61 | return text[1 : len(text)-1], empty, true 62 | } 63 | 64 | return text[1:s], text[s+1 : len(text)-1], true 65 | } 66 | 67 | // Encode returns the IRC message text for CTCP tagged data. 68 | // 69 | // ::= [ ] 70 | // ::= 0x01 71 | // 72 | func Encode(tag, message string) (text string) { 73 | 74 | switch { 75 | 76 | // We can't build a valid CTCP tagged message without at least a tag. 77 | case len(tag) <= 0: 78 | return empty 79 | 80 | // Tagged data with a message 81 | case len(message) > 0: 82 | return string(delimiter) + tag + string(space) + message + string(delimiter) 83 | 84 | // Tagged data without a message 85 | default: 86 | return string(delimiter) + tag + string(delimiter) 87 | 88 | } 89 | } 90 | 91 | // Action is a shortcut for Encode(ctcp.ACTION, message). 92 | func Action(message string) string { 93 | return Encode(ACTION, message) 94 | } 95 | 96 | // Ping is a shortcut for Encode(ctcp.PING, message). 97 | func Ping(message string) string { 98 | return Encode(PING, message) 99 | } 100 | 101 | // Pong is a shortcut for Encode(ctcp.PONG, message). 102 | func Pong(message string) string { 103 | return Encode(PONG, message) 104 | } 105 | 106 | // Version is a shortcut for Encode(ctcp.VERSION, message). 107 | func Version(message string) string { 108 | return Encode(VERSION, message) 109 | } 110 | 111 | // VersionReply is a shortcut for ENCODE(ctcp.VERSION, go version info). 112 | func VersionReply() string { 113 | return Encode(VERSION, fmt.Sprintf(versionFormat, runtime.Version())) 114 | } 115 | 116 | // UserInfo is a shortcut for Encode(ctcp.USERINFO, message). 117 | func UserInfo(message string) string { 118 | return Encode(USERINFO, message) 119 | } 120 | 121 | // ClientInfo is a shortcut for Encode(ctcp.CLIENTINFO, message). 122 | func ClientInfo(message string) string { 123 | return Encode(CLIENTINFO, message) 124 | } 125 | 126 | // Finger is a shortcut for Encode(ctcp.FINGER, message). 127 | func Finger(message string) string { 128 | return Encode(FINGER, message) 129 | } 130 | 131 | // Source is a shortcut for Encode(ctcp.SOURCE, message). 132 | func Source(message string) string { 133 | return Encode(SOURCE, message) 134 | } 135 | 136 | // Time is a shortcut for Encode(ctcp.TIME, message). 137 | func Time(message string) string { 138 | return Encode(TIME, message) 139 | } 140 | 141 | // TimeReply is a shortcut for Encode(ctcp.TIME, currenttime). 142 | func TimeReply() string { 143 | return Encode(TIME, time.Now().Format(timeFormat)) 144 | } 145 | -------------------------------------------------------------------------------- /ctcp/ctcp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | package ctcp 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestDecode(t *testing.T) { 12 | if _, _, ok := Decode("\x01\x01"); ok { 13 | t.Error("Message is invalid, but ok is true.") 14 | } 15 | if _, _, ok := Decode("\x01"); ok { 16 | t.Error("Message is invalid, but ok is true.") 17 | } 18 | if _, _, ok := Decode("\x01VERSION"); ok { 19 | t.Error("Message is invalid, but ok is true.") 20 | } 21 | if tag, message, ok := Decode("\x01VERSION\x01"); tag != "VERSION" || len(message) > 0 || !ok { 22 | t.Error("Message contains only a tag, wrong results.") 23 | } 24 | if tag, message, ok := Decode("\x01PING 123456789\x01"); tag != "PING" || message != "123456789" || !ok { 25 | t.Error("Message contains tag and a message, wrong results.") 26 | } 27 | if tag, message, ok := Decode("\x01CLIENTINFO A B C\x01"); tag != "CLIENTINFO" || message != "A B C" || !ok { 28 | t.Error("Message contains tag and a message with spaces, wrong results.") 29 | } 30 | } 31 | 32 | func TestEncode(t *testing.T) { 33 | if text := Encode("", "INVALID"); len(text) > 0 { 34 | t.Error("Message is invalid, but returns a non-empty string.") 35 | } 36 | if text := Encode("VERSION", ""); text != "\x01VERSION\x01" { 37 | t.Error("Message contains only a tag, wrong results.") 38 | } 39 | if text := Encode("PING", "123456789"); text != "\x01PING 123456789\x01" { 40 | t.Error("Message contains tag and a message, wrong results.") 41 | } 42 | if text := Encode("CLIENTINFO", "A B C"); text != "\x01CLIENTINFO A B C\x01" { 43 | t.Error("Message contains tag and a message with spaces, wrong results.") 44 | } 45 | } 46 | 47 | func TestAction(t *testing.T) { 48 | if text := Action("A B C"); text != "\x01ACTION A B C\x01" { 49 | t.Error("Wrong result!") 50 | } 51 | } 52 | 53 | func TestPing(t *testing.T) { 54 | if text := Ping("A B C"); text != "\x01PING A B C\x01" { 55 | t.Error("Wrong result!") 56 | } 57 | } 58 | 59 | func TestPong(t *testing.T) { 60 | if text := Pong("A B C"); text != "\x01PONG A B C\x01" { 61 | t.Error("Wrong result!") 62 | } 63 | } 64 | 65 | func TestVersion(t *testing.T) { 66 | if text := Version("A B C"); text != "\x01VERSION A B C\x01" { 67 | t.Error("Wrong result!") 68 | } 69 | } 70 | 71 | func TestUserInfo(t *testing.T) { 72 | if text := UserInfo("A B C"); text != "\x01USERINFO A B C\x01" { 73 | t.Error("Wrong result!") 74 | } 75 | } 76 | 77 | func TestClientInfo(t *testing.T) { 78 | if text := ClientInfo("A B C"); text != "\x01CLIENTINFO A B C\x01" { 79 | t.Error("Wrong result!") 80 | } 81 | } 82 | 83 | func TestFinger(t *testing.T) { 84 | if text := Finger("A B C"); text != "\x01FINGER A B C\x01" { 85 | t.Error("Wrong result!") 86 | } 87 | } 88 | 89 | func TestSource(t *testing.T) { 90 | if text := Source("A B C"); text != "\x01SOURCE A B C\x01" { 91 | t.Error("Wrong result!") 92 | } 93 | } 94 | 95 | func TestTime(t *testing.T) { 96 | if text := Time("A B C"); text != "\x01TIME A B C\x01" { 97 | t.Error("Wrong result!") 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ctcp/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | // Package ctcp implements partial support for the Client-to-Client Protocol. 6 | // 7 | // CTCP defines extended messages using the standard PRIVMSG and NOTICE 8 | // commands in IRC. This means that any CTCP messages are embedded inside the 9 | // normal message text. Clients that don't support CTCP simply show 10 | // the encoded message to the user. 11 | // 12 | // Most IRC clients support only a subset of the protocol, and only a few 13 | // commands are actually used. This package aims to implement the most basic 14 | // CTCP messages: a single command per IRC message. Quoting is not supported. 15 | // 16 | // Example using the irc.Message type: 17 | // 18 | // m := irc.ParseMessage(...) 19 | // 20 | // if tag, text, ok := ctcp.Decode(m.Trailing); ok { 21 | // // This is a CTCP message. 22 | // } else { 23 | // // This is not a CTCP message. 24 | // } 25 | // 26 | // Similar, for encoding messages: 27 | // 28 | // m.Trailing = ctcp.Encode("ACTION","wants a cookie!") 29 | // 30 | // Do not send a complete IRC message to Decode, it won't work. 31 | package ctcp 32 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | // Package irc allows your application to speak the IRC protocol. 6 | // 7 | // The Message and Prefix structs provide translation to and from raw IRC messages: 8 | // 9 | // // Parse the IRC-encoded data and store the result in a new struct: 10 | // message := irc.ParseMessage(raw) 11 | // 12 | // // Translate back to a raw IRC message string: 13 | // raw = message.String() 14 | // 15 | // Decoder and Encoder can be used to decode and encode messages in a stream: 16 | // 17 | // // Create a decoder that reads from given io.Reader 18 | // dec := irc.NewDecoder(reader) 19 | // 20 | // // Decode the next IRC message 21 | // message, err := dec.Decode() 22 | // 23 | // // Create an encoder that writes to given io.Writer 24 | // enc := irc.NewEncoder(writer) 25 | // 26 | // // Send a message to the writer. 27 | // enc.Encode(message) 28 | // 29 | // The Conn type combines an Encoder and Decoder for a duplex connection. 30 | // 31 | // c, err := irc.Dial("irc.server.net:6667") 32 | // 33 | // // Methods from both Encoder and Decoder are available 34 | // message, err := c.Decode() 35 | // 36 | package irc 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sorcix/irc 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | package irc 6 | 7 | import ( 8 | "bytes" 9 | "strings" 10 | ) 11 | 12 | // Various constants used for formatting IRC messages. 13 | const ( 14 | prefix byte = 0x3A // Prefix or last argument 15 | prefixUser byte = 0x21 // Username 16 | prefixHost byte = 0x40 // Hostname 17 | space byte = 0x20 // Separator 18 | 19 | maxLength = 510 // Maximum length is 512 - 2 for the line endings. 20 | ) 21 | 22 | func cutsetFunc(r rune) bool { 23 | // Characters to trim from prefixes/messages. 24 | return r == '\r' || r == '\n' 25 | } 26 | 27 | // Sender represents objects that are able to send messages to an IRC server. 28 | // 29 | // As there might be a message queue, it is possible that Send returns a nil 30 | // error, but the message is not sent (yet). The error value is only used when 31 | // it is certain that sending the message is impossible. 32 | // 33 | // This interface is not used inside this package, and shouldn't have been 34 | // defined here in the first place. For backwards compatibility only. 35 | type Sender interface { 36 | Send(*Message) error 37 | } 38 | 39 | // Prefix represents the prefix (sender) of an IRC message. 40 | // See RFC1459 section 2.3.1. 41 | // 42 | // | [ '!' ] [ '@' ] 43 | // 44 | type Prefix struct { 45 | Name string // Nick- or servername 46 | User string // Username 47 | Host string // Hostname 48 | } 49 | 50 | // ParsePrefix takes a string and attempts to create a Prefix struct. 51 | func ParsePrefix(raw string) (p *Prefix) { 52 | 53 | p = new(Prefix) 54 | 55 | user := indexByte(raw, prefixUser) 56 | host := indexByte(raw, prefixHost) 57 | 58 | switch { 59 | 60 | case user > 0 && host > user: 61 | p.Name = raw[:user] 62 | p.User = raw[user+1 : host] 63 | p.Host = raw[host+1:] 64 | 65 | case user > 0: 66 | p.Name = raw[:user] 67 | p.User = raw[user+1:] 68 | 69 | case host > 0: 70 | p.Name = raw[:host] 71 | p.Host = raw[host+1:] 72 | 73 | default: 74 | p.Name = raw 75 | 76 | } 77 | 78 | return p 79 | } 80 | 81 | // Len calculates the length of the string representation of this prefix. 82 | func (p *Prefix) Len() (length int) { 83 | length = len(p.Name) 84 | if len(p.User) > 0 { 85 | length = length + len(p.User) + 1 86 | } 87 | if len(p.Host) > 0 { 88 | length = length + len(p.Host) + 1 89 | } 90 | return 91 | } 92 | 93 | // Bytes returns a []byte representation of this prefix. 94 | func (p *Prefix) Bytes() []byte { 95 | buffer := new(bytes.Buffer) 96 | p.writeTo(buffer) 97 | return buffer.Bytes() 98 | } 99 | 100 | // String returns a string representation of this prefix. 101 | func (p *Prefix) String() (s string) { 102 | // Benchmarks revealed that in this case simple string concatenation 103 | // is actually faster than using a ByteBuffer as in (*Message).String() 104 | s = p.Name 105 | if len(p.User) > 0 { 106 | s = s + string(prefixUser) + p.User 107 | } 108 | if len(p.Host) > 0 { 109 | s = s + string(prefixHost) + p.Host 110 | } 111 | return 112 | } 113 | 114 | // IsHostmask returns true if this prefix looks like a user hostmask. 115 | func (p *Prefix) IsHostmask() bool { 116 | return len(p.User) > 0 && len(p.Host) > 0 117 | } 118 | 119 | // IsServer returns true if this prefix looks like a server name. 120 | func (p *Prefix) IsServer() bool { 121 | return len(p.User) <= 0 && len(p.Host) <= 0 // && indexByte(p.Name, '.') > 0 122 | } 123 | 124 | // writeTo is an utility function to write the prefix to the bytes.Buffer in Message.String(). 125 | func (p *Prefix) writeTo(buffer *bytes.Buffer) { 126 | buffer.WriteString(p.Name) 127 | if len(p.User) > 0 { 128 | buffer.WriteByte(prefixUser) 129 | buffer.WriteString(p.User) 130 | } 131 | if len(p.Host) > 0 { 132 | buffer.WriteByte(prefixHost) 133 | buffer.WriteString(p.Host) 134 | } 135 | return 136 | } 137 | 138 | // Message represents an IRC protocol message. 139 | // See RFC1459 section 2.3.1. 140 | // 141 | // ::= [':' ] 142 | // ::= | [ '!' ] [ '@' ] 143 | // ::= { } | 144 | // ::= ' ' { ' ' } 145 | // ::= [ ':' | ] 146 | // 147 | // ::= 149 | // ::= 151 | // 152 | // ::= CR LF 153 | type Message struct { 154 | *Prefix 155 | Command string 156 | Params []string 157 | Trailing string 158 | 159 | // When set to true, the trailing prefix (:) will be added even if the trailing message is empty. 160 | EmptyTrailing bool 161 | } 162 | 163 | // ParseMessage takes a string and attempts to create a Message struct. 164 | // Returns nil if the Message is invalid. 165 | func ParseMessage(raw string) (m *Message) { 166 | 167 | // Ignore empty messages. 168 | if raw = strings.TrimFunc(raw, cutsetFunc); len(raw) < 2 { 169 | return nil 170 | } 171 | 172 | i, j := 0, 0 173 | 174 | m = new(Message) 175 | 176 | if raw[0] == prefix { 177 | 178 | // Prefix ends with a space. 179 | i = indexByte(raw, space) 180 | 181 | // Prefix string must not be empty if the indicator is present. 182 | if i < 2 { 183 | return nil 184 | } 185 | 186 | m.Prefix = ParsePrefix(raw[1:i]) 187 | 188 | // Skip space at the end of the prefix 189 | i++ 190 | } 191 | 192 | // Find end of command 193 | j = i + indexByte(raw[i:], space) 194 | 195 | // Extract command 196 | if j > i { 197 | m.Command = strings.ToUpper(raw[i:j]) 198 | } else { 199 | m.Command = strings.ToUpper(raw[i:]) 200 | 201 | // We're done here! 202 | return m 203 | } 204 | 205 | // Skip space after command 206 | j++ 207 | 208 | // Find prefix for trailer 209 | i = indexByte(raw[j:], prefix) 210 | 211 | if i < 0 || raw[j+i-1] != space { 212 | 213 | // There is no trailing argument! 214 | m.Params = strings.Split(raw[j:], string(space)) 215 | 216 | // We're done here! 217 | return m 218 | } 219 | 220 | // Compensate for index on substring 221 | i = i + j 222 | 223 | // Check if we need to parse arguments. 224 | if i > j { 225 | m.Params = strings.Split(raw[j:i-1], string(space)) 226 | } 227 | 228 | m.Trailing = raw[i+1:] 229 | 230 | // We need to re-encode the trailing argument even if it was empty. 231 | if len(m.Trailing) <= 0 { 232 | m.EmptyTrailing = true 233 | } 234 | 235 | return m 236 | 237 | } 238 | 239 | // Len calculates the length of the string representation of this message. 240 | func (m *Message) Len() (length int) { 241 | 242 | if m.Prefix != nil { 243 | length = m.Prefix.Len() + 2 // Include prefix and trailing space 244 | } 245 | 246 | length = length + len(m.Command) 247 | 248 | if len(m.Params) > 0 { 249 | length = length + len(m.Params) 250 | for _, param := range m.Params { 251 | length = length + len(param) 252 | } 253 | } 254 | 255 | if len(m.Trailing) > 0 || m.EmptyTrailing { 256 | length = length + len(m.Trailing) + 2 // Include prefix and space 257 | } 258 | 259 | return 260 | } 261 | 262 | // Bytes returns a []byte representation of this message. 263 | // 264 | // As noted in rfc2812 section 2.3, messages should not exceed 512 characters 265 | // in length. This method forces that limit by discarding any characters 266 | // exceeding the length limit. 267 | func (m *Message) Bytes() []byte { 268 | 269 | buffer := new(bytes.Buffer) 270 | 271 | // Message prefix 272 | if m.Prefix != nil { 273 | buffer.WriteByte(prefix) 274 | m.Prefix.writeTo(buffer) 275 | buffer.WriteByte(space) 276 | } 277 | 278 | // Command is required 279 | buffer.WriteString(m.Command) 280 | 281 | // Space separated list of arguments 282 | if len(m.Params) > 0 { 283 | buffer.WriteByte(space) 284 | buffer.WriteString(strings.Join(m.Params, string(space))) 285 | } 286 | 287 | if len(m.Trailing) > 0 || m.EmptyTrailing { 288 | buffer.WriteByte(space) 289 | buffer.WriteByte(prefix) 290 | buffer.WriteString(m.Trailing) 291 | } 292 | 293 | // We need the limit the buffer length. 294 | if buffer.Len() > (maxLength) { 295 | buffer.Truncate(maxLength) 296 | } 297 | 298 | return buffer.Bytes() 299 | } 300 | 301 | // String returns a string representation of this message. 302 | // 303 | // As noted in rfc2812 section 2.3, messages should not exceed 512 characters 304 | // in length. This method forces that limit by discarding any characters 305 | // exceeding the length limit. 306 | func (m *Message) String() string { 307 | return string(m.Bytes()) 308 | } 309 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | package irc 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func ExampleParseMessage() { 14 | message := ParseMessage("JOIN #help") 15 | 16 | fmt.Println(message.Params[0]) 17 | 18 | // Output: #help 19 | } 20 | 21 | func ExampleMessage_String() { 22 | message := &Message{ 23 | Prefix: &Prefix{ 24 | Name: "sorcix", 25 | User: "sorcix", 26 | Host: "myhostname", 27 | }, 28 | Command: "PRIVMSG", 29 | Trailing: "This is an example!", 30 | } 31 | 32 | fmt.Println(message.String()) 33 | 34 | // Output: :sorcix!sorcix@myhostname PRIVMSG :This is an example! 35 | } 36 | 37 | var messageTests = [...]*struct { 38 | parsed *Message 39 | rawMessage string 40 | rawPrefix string 41 | hostmask bool // Is it very clear that the prefix is a hostname? 42 | server bool // Is the prefix a servername? 43 | }{ 44 | { 45 | parsed: &Message{ 46 | Prefix: &Prefix{ 47 | Name: "syrk", 48 | User: "kalt", 49 | Host: "millennium.stealth.net", 50 | }, 51 | Command: "QUIT", 52 | Trailing: "Gone to have lunch", 53 | }, 54 | rawMessage: ":syrk!kalt@millennium.stealth.net QUIT :Gone to have lunch", 55 | rawPrefix: "syrk!kalt@millennium.stealth.net", 56 | hostmask: true, 57 | }, 58 | { 59 | parsed: &Message{ 60 | Prefix: &Prefix{ 61 | Name: "Trillian", 62 | }, 63 | Command: "SQUIT", 64 | Params: []string{"cm22.eng.umd.edu"}, 65 | Trailing: "Server out of control", 66 | }, 67 | rawMessage: ":Trillian SQUIT cm22.eng.umd.edu :Server out of control", 68 | rawPrefix: "Trillian", 69 | server: true, 70 | }, 71 | { 72 | parsed: &Message{ 73 | Prefix: &Prefix{ 74 | Name: "WiZ", 75 | User: "jto", 76 | Host: "tolsun.oulu.fi", 77 | }, 78 | Command: "JOIN", 79 | Params: []string{"#Twilight_zone"}, 80 | }, 81 | rawMessage: ":WiZ!jto@tolsun.oulu.fi JOIN #Twilight_zone", 82 | rawPrefix: "WiZ!jto@tolsun.oulu.fi", 83 | hostmask: true, 84 | }, 85 | { 86 | parsed: &Message{ 87 | Prefix: &Prefix{ 88 | Name: "WiZ", 89 | User: "jto", 90 | Host: "tolsun.oulu.fi", 91 | }, 92 | Command: "PART", 93 | Params: []string{"#playzone"}, 94 | Trailing: "I lost", 95 | }, 96 | rawMessage: ":WiZ!jto@tolsun.oulu.fi PART #playzone :I lost", 97 | rawPrefix: "WiZ!jto@tolsun.oulu.fi", 98 | hostmask: true, 99 | }, 100 | { 101 | parsed: &Message{ 102 | Prefix: &Prefix{ 103 | Name: "WiZ", 104 | User: "jto", 105 | Host: "tolsun.oulu.fi", 106 | }, 107 | Command: "MODE", 108 | Params: []string{"#eu-opers", "-l"}, 109 | }, 110 | rawMessage: ":WiZ!jto@tolsun.oulu.fi MODE #eu-opers -l", 111 | rawPrefix: "WiZ!jto@tolsun.oulu.fi", 112 | hostmask: true, 113 | }, 114 | { 115 | parsed: &Message{ 116 | Command: "MODE", 117 | Params: []string{"&oulu", "+b", "*!*@*.edu", "+e", "*!*@*.bu.edu"}, 118 | }, 119 | rawMessage: "MODE &oulu +b *!*@*.edu +e *!*@*.bu.edu", 120 | }, 121 | { 122 | parsed: &Message{ 123 | Command: "PRIVMSG", 124 | Params: []string{"#channel"}, 125 | Trailing: "Message with :colons!", 126 | }, 127 | rawMessage: "PRIVMSG #channel :Message with :colons!", 128 | }, 129 | { 130 | parsed: &Message{ 131 | Prefix: &Prefix{ 132 | Name: "irc.vives.lan", 133 | }, 134 | Command: "251", 135 | Params: []string{"test"}, 136 | Trailing: "There are 2 users and 0 services on 1 servers", 137 | }, 138 | rawMessage: ":irc.vives.lan 251 test :There are 2 users and 0 services on 1 servers", 139 | rawPrefix: "irc.vives.lan", 140 | server: true, 141 | }, 142 | { 143 | parsed: &Message{ 144 | Prefix: &Prefix{ 145 | Name: "irc.vives.lan", 146 | }, 147 | Command: "376", 148 | Params: []string{"test"}, 149 | Trailing: "End of MOTD command", 150 | }, 151 | rawMessage: ":irc.vives.lan 376 test :End of MOTD command", 152 | rawPrefix: "irc.vives.lan", 153 | server: true, 154 | }, 155 | { 156 | parsed: &Message{ 157 | Prefix: &Prefix{ 158 | Name: "irc.vives.lan", 159 | }, 160 | Command: "250", 161 | Params: []string{"test"}, 162 | Trailing: "Highest connection count: 1 (1 connections received)", 163 | }, 164 | rawMessage: ":irc.vives.lan 250 test :Highest connection count: 1 (1 connections received)", 165 | rawPrefix: "irc.vives.lan", 166 | server: true, 167 | }, 168 | { 169 | parsed: &Message{ 170 | Prefix: &Prefix{ 171 | Name: "sorcix", 172 | User: "~sorcix", 173 | Host: "sorcix.users.quakenet.org", 174 | }, 175 | Command: "PRIVMSG", 176 | Params: []string{"#viveslan"}, 177 | Trailing: "\001ACTION is testing CTCP messages!\001", 178 | }, 179 | rawMessage: ":sorcix!~sorcix@sorcix.users.quakenet.org PRIVMSG #viveslan :\001ACTION is testing CTCP messages!\001", 180 | rawPrefix: "sorcix!~sorcix@sorcix.users.quakenet.org", 181 | hostmask: true, 182 | }, 183 | { 184 | parsed: &Message{ 185 | Prefix: &Prefix{ 186 | Name: "sorcix", 187 | User: "~sorcix", 188 | Host: "sorcix.users.quakenet.org", 189 | }, 190 | Command: "NOTICE", 191 | Params: []string{"midnightfox"}, 192 | Trailing: "\001PONG 1234567890\001", 193 | }, 194 | rawMessage: ":sorcix!~sorcix@sorcix.users.quakenet.org NOTICE midnightfox :\001PONG 1234567890\001", 195 | rawPrefix: "sorcix!~sorcix@sorcix.users.quakenet.org", 196 | hostmask: true, 197 | }, 198 | { 199 | parsed: &Message{ 200 | Prefix: &Prefix{ 201 | Name: "a", 202 | User: "b", 203 | Host: "c", 204 | }, 205 | Command: "QUIT", 206 | }, 207 | rawMessage: ":a!b@c QUIT", 208 | rawPrefix: "a!b@c", 209 | hostmask: true, 210 | }, 211 | { 212 | parsed: &Message{ 213 | Prefix: &Prefix{ 214 | Name: "a", 215 | User: "b", 216 | }, 217 | Command: "PRIVMSG", 218 | Trailing: "message", 219 | }, 220 | rawMessage: ":a!b PRIVMSG :message", 221 | rawPrefix: "a!b", 222 | }, 223 | { 224 | parsed: &Message{ 225 | Prefix: &Prefix{ 226 | Name: "a", 227 | Host: "c", 228 | }, 229 | Command: "NOTICE", 230 | Trailing: ":::Hey!", 231 | }, 232 | rawMessage: ":a@c NOTICE ::::Hey!", 233 | rawPrefix: "a@c", 234 | }, 235 | { 236 | parsed: &Message{ 237 | Prefix: &Prefix{ 238 | Name: "nick", 239 | }, 240 | Command: "PRIVMSG", 241 | Params: []string{"$@"}, 242 | Trailing: "This message contains a\ttab!", 243 | }, 244 | rawMessage: ":nick PRIVMSG $@ :This message contains a\ttab!", 245 | rawPrefix: "nick", 246 | }, 247 | { 248 | parsed: &Message{ 249 | Command: "TEST", 250 | Params: []string{"$@", "", "param"}, 251 | Trailing: "Trailing", 252 | }, 253 | rawMessage: "TEST $@ param :Trailing", 254 | }, 255 | { 256 | rawMessage: ": PRIVMSG test :Invalid message with empty prefix.", 257 | rawPrefix: "", 258 | }, 259 | { 260 | rawMessage: ": PRIVMSG test :Invalid message with space prefix", 261 | rawPrefix: " ", 262 | }, 263 | { 264 | parsed: &Message{ 265 | Command: "TOPIC", 266 | Params: []string{"#foo"}, 267 | Trailing: "", 268 | }, 269 | rawMessage: "TOPIC #foo", 270 | rawPrefix: "", 271 | }, 272 | { 273 | parsed: &Message{ 274 | Command: "TOPIC", 275 | Params: []string{"#foo"}, 276 | Trailing: "", 277 | EmptyTrailing: true, 278 | }, 279 | rawMessage: "TOPIC #foo :", 280 | rawPrefix: "", 281 | }, 282 | { 283 | parsed: &Message{ 284 | Prefix: &Prefix{ 285 | Name: "name", 286 | User: "user", 287 | Host: "example.org", 288 | }, 289 | Command: "PRIVMSG", 290 | Params: []string{"#test"}, 291 | Trailing: "Message with spaces at the end! ", 292 | }, 293 | rawMessage: ":name!user@example.org PRIVMSG #test :Message with spaces at the end! ", 294 | rawPrefix: "name!user@example.org", 295 | hostmask: true, 296 | }, 297 | { 298 | parsed: &Message{ 299 | Command: "PASS", 300 | Params: []string{"oauth:token_goes_here"}, 301 | }, 302 | rawMessage: "PASS oauth:token_goes_here", 303 | rawPrefix: "", 304 | }, 305 | } 306 | 307 | // ----- 308 | // PREFIX 309 | // ----- 310 | 311 | func TestPrefix_IsHostmask(t *testing.T) { 312 | 313 | for i, test := range messageTests { 314 | 315 | // Skip tests that have no prefix 316 | if test.parsed == nil || test.parsed.Prefix == nil { 317 | continue 318 | } 319 | 320 | if test.hostmask && !test.parsed.Prefix.IsHostmask() { 321 | t.Errorf("Prefix %d should be recognized as a hostmask!", i) 322 | } 323 | 324 | } 325 | } 326 | 327 | func TestPrefix_IsServer(t *testing.T) { 328 | 329 | for i, test := range messageTests { 330 | 331 | // Skip tests that have no prefix 332 | if test.parsed == nil || test.parsed.Prefix == nil { 333 | continue 334 | } 335 | 336 | if test.server && !test.parsed.Prefix.IsServer() { 337 | t.Errorf("Prefix %d should be recognized as a server!", i) 338 | } 339 | 340 | } 341 | } 342 | 343 | func TestPrefix_String(t *testing.T) { 344 | var s string 345 | 346 | for i, test := range messageTests { 347 | 348 | // Skip tests that have no prefix 349 | if test.parsed == nil || test.parsed.Prefix == nil { 350 | continue 351 | } 352 | 353 | // Convert the prefix 354 | s = test.parsed.Prefix.String() 355 | 356 | // Result should be the same as the value in rawMessage. 357 | if s != test.rawPrefix { 358 | t.Errorf("Failed to stringify prefix %d:", i) 359 | t.Logf("Output: %s", s) 360 | t.Logf("Expected: %s", test.rawPrefix) 361 | } 362 | } 363 | } 364 | 365 | func TestPrefix_Len(t *testing.T) { 366 | var l int 367 | 368 | for i, test := range messageTests { 369 | 370 | // Skip tests that have no prefix 371 | if test.parsed == nil || test.parsed.Prefix == nil { 372 | continue 373 | } 374 | 375 | l = test.parsed.Prefix.Len() 376 | 377 | // Result should be the same as the value in rawMessage. 378 | if l != len(test.rawPrefix) { 379 | t.Errorf("Failed to calculate prefix length %d:", i) 380 | t.Logf("Output: %d", l) 381 | t.Logf("Expected: %d", len(test.rawPrefix)) 382 | } 383 | } 384 | } 385 | 386 | func TestParsePrefix(t *testing.T) { 387 | var p *Prefix 388 | 389 | for i, test := range messageTests { 390 | 391 | // Skip tests that have no prefix 392 | if test.parsed == nil || test.parsed.Prefix == nil { 393 | continue 394 | } 395 | 396 | // Parse the prefix 397 | p = ParsePrefix(test.rawPrefix) 398 | 399 | // Result struct should be the same as the value in parsed. 400 | if *p != *test.parsed.Prefix { 401 | t.Errorf("Failed to parse prefix %d:", i) 402 | t.Logf("Output: %#v", p) 403 | t.Logf("Expected: %#v", test.parsed.Prefix) 404 | } 405 | } 406 | } 407 | 408 | // ----- 409 | // MESSAGE 410 | // ----- 411 | 412 | func TestMessage_String(t *testing.T) { 413 | var s string 414 | 415 | for i, test := range messageTests { 416 | 417 | // Skip tests that have no valid struct 418 | if test.parsed == nil { 419 | continue 420 | } 421 | 422 | // Convert the prefix 423 | s = test.parsed.String() 424 | 425 | // Result should be the same as the value in rawMessage. 426 | if s != test.rawMessage { 427 | t.Errorf("Failed to stringify message %d:", i) 428 | t.Logf("Output: %s", s) 429 | t.Logf("Expected: %s", test.rawMessage) 430 | } 431 | } 432 | } 433 | 434 | func TestMessage_Len(t *testing.T) { 435 | var l int 436 | 437 | for i, test := range messageTests { 438 | 439 | // Skip tests that have no valid struct 440 | if test.parsed == nil { 441 | continue 442 | } 443 | 444 | l = test.parsed.Len() 445 | 446 | // Result should be the same as the value in rawMessage. 447 | if l != len(test.rawMessage) { 448 | t.Errorf("Failed to calculate message length %d:", i) 449 | t.Logf("Output: %d", l) 450 | t.Logf("Expected: %d", len(test.rawMessage)) 451 | } 452 | } 453 | } 454 | 455 | func TestParseMessage(t *testing.T) { 456 | var p *Message 457 | 458 | for i, test := range messageTests { 459 | 460 | // Parse the prefix 461 | p = ParseMessage(test.rawMessage) 462 | 463 | // Result struct should be the same as the value in parsed. 464 | if !reflect.DeepEqual(p, test.parsed) { 465 | t.Errorf("Failed to parse message %d:", i) 466 | t.Logf("Output: %#v", p) 467 | t.Logf("Expected: %#v", test.parsed) 468 | } 469 | } 470 | } 471 | 472 | // ----- 473 | // MESSAGE DECODE -> ENCODE 474 | // ----- 475 | 476 | func TestMessageDecodeEncode(t *testing.T) { 477 | var ( 478 | p *Message 479 | s string 480 | ) 481 | 482 | for i, test := range messageTests { 483 | 484 | // Skip invalid messages 485 | if test.parsed == nil { 486 | continue 487 | } 488 | 489 | // Decode the message, then encode it again. 490 | p = ParseMessage(test.rawMessage) 491 | s = p.String() 492 | 493 | // Result struct should be the same as the original. 494 | if s != test.rawMessage { 495 | t.Errorf("Message %d failed decode-encode sequence!", i) 496 | } 497 | } 498 | } 499 | 500 | // ----- 501 | // BENCHMARK 502 | // ----- 503 | 504 | func BenchmarkPrefix_String_short(b *testing.B) { 505 | b.ReportAllocs() 506 | 507 | prefix := new(Prefix) 508 | prefix.Name = "Namename" 509 | 510 | b.ResetTimer() 511 | for i := 0; i < b.N; i++ { 512 | prefix.String() 513 | } 514 | } 515 | func BenchmarkPrefix_String_long(b *testing.B) { 516 | b.ReportAllocs() 517 | 518 | prefix := new(Prefix) 519 | prefix.Name = "Namename" 520 | prefix.User = "Username" 521 | prefix.Host = "Hostname" 522 | 523 | b.ResetTimer() 524 | for i := 0; i < b.N; i++ { 525 | prefix.String() 526 | } 527 | } 528 | func BenchmarkParsePrefix_short(b *testing.B) { 529 | b.ReportAllocs() 530 | 531 | for i := 0; i < b.N; i++ { 532 | ParsePrefix("Namename") 533 | } 534 | } 535 | func BenchmarkParsePrefix_long(b *testing.B) { 536 | b.ReportAllocs() 537 | 538 | for i := 0; i < b.N; i++ { 539 | ParsePrefix("Namename!Username@Hostname") 540 | } 541 | } 542 | func BenchmarkMessage_String(b *testing.B) { 543 | b.ReportAllocs() 544 | 545 | for i := 0; i < b.N; i++ { 546 | messageTests[0].parsed.String() 547 | } 548 | } 549 | func BenchmarkParseMessage_short(b *testing.B) { 550 | b.ReportAllocs() 551 | 552 | for i := 0; i < b.N; i++ { 553 | ParseMessage("COMMAND arg1 :Message\r\n") 554 | } 555 | } 556 | func BenchmarkParseMessage_medium(b *testing.B) { 557 | b.ReportAllocs() 558 | 559 | for i := 0; i < b.N; i++ { 560 | ParseMessage(":Namename COMMAND arg6 arg7 :Message message message\r\n") 561 | } 562 | } 563 | func BenchmarkParseMessage_long(b *testing.B) { 564 | b.ReportAllocs() 565 | 566 | for i := 0; i < b.N; i++ { 567 | ParseMessage(":Namename!username@hostname COMMAND arg1 arg2 arg3 arg4 arg5 arg6 arg7 :Message message message message message\r\n") 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | package irc 6 | 7 | import ( 8 | "bufio" 9 | "io" 10 | "net" 11 | "sync" 12 | ) 13 | 14 | // Messages are delimited with CR and LF line endings, 15 | // we're using the last one to split the stream. Both are removed 16 | // during message parsing. 17 | const delim byte = '\n' 18 | 19 | var endline = []byte("\r\n") 20 | 21 | // A Conn represents an IRC network protocol connection. 22 | // It consists of an Encoder and Decoder to manage I/O. 23 | type Conn struct { 24 | Encoder 25 | Decoder 26 | 27 | conn io.ReadWriteCloser 28 | } 29 | 30 | // NewConn returns a new Conn using rwc for I/O. 31 | func NewConn(rwc io.ReadWriteCloser) *Conn { 32 | return &Conn{ 33 | Encoder: Encoder{ 34 | writer: rwc, 35 | }, 36 | Decoder: Decoder{ 37 | reader: bufio.NewReader(rwc), 38 | }, 39 | conn: rwc, 40 | } 41 | } 42 | 43 | // Dial connects to the given address using net.Dial and 44 | // then returns a new Conn for the connection. 45 | func Dial(addr string) (*Conn, error) { 46 | c, err := net.Dial("tcp", addr) 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return NewConn(c), nil 53 | } 54 | 55 | // Close closes the underlying ReadWriteCloser. 56 | func (c *Conn) Close() error { 57 | return c.conn.Close() 58 | } 59 | 60 | // A Decoder reads Message objects from an input stream. 61 | type Decoder struct { 62 | reader *bufio.Reader 63 | line string 64 | mu sync.Mutex 65 | } 66 | 67 | // NewDecoder returns a new Decoder that reads from r. 68 | func NewDecoder(r io.Reader) *Decoder { 69 | return &Decoder{ 70 | reader: bufio.NewReader(r), 71 | } 72 | } 73 | 74 | // Decode attempts to read a single Message from the stream. 75 | // 76 | // Returns a non-nil error if the read failed. 77 | func (dec *Decoder) Decode() (m *Message, err error) { 78 | 79 | dec.mu.Lock() 80 | dec.line, err = dec.reader.ReadString(delim) 81 | dec.mu.Unlock() 82 | 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return ParseMessage(dec.line), nil 88 | } 89 | 90 | // An Encoder writes Message objects to an output stream. 91 | type Encoder struct { 92 | writer io.Writer 93 | mu sync.Mutex 94 | } 95 | 96 | // NewEncoder returns a new Encoder that writes to w. 97 | func NewEncoder(w io.Writer) *Encoder { 98 | return &Encoder{ 99 | writer: w, 100 | } 101 | } 102 | 103 | // Encode writes the IRC encoding of m to the stream. 104 | // 105 | // This method may be used from multiple goroutines. 106 | // 107 | // Returns an non-nil error if the write to the underlying stream stopped early. 108 | func (enc *Encoder) Encode(m *Message) (err error) { 109 | 110 | _, err = enc.Write(m.Bytes()) 111 | 112 | return 113 | } 114 | 115 | // Write writes len(p) bytes from p followed by CR+LF. 116 | // 117 | // This method can be used simultaneously from multiple goroutines, 118 | // it guarantees to serialize access. However, writing a single IRC message 119 | // using multiple Write calls will cause corruption. 120 | func (enc *Encoder) Write(p []byte) (n int, err error) { 121 | 122 | enc.mu.Lock() 123 | n, err = enc.writer.Write(p) 124 | 125 | if err != nil { 126 | enc.mu.Unlock() 127 | return 128 | } 129 | 130 | _, err = enc.writer.Write(endline) 131 | enc.mu.Unlock() 132 | 133 | return 134 | } 135 | -------------------------------------------------------------------------------- /stream_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | package irc 6 | 7 | import ( 8 | "bytes" 9 | "crypto/tls" 10 | "io" 11 | "log" 12 | "reflect" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | // We use the Dial function as a simple shortcut for connecting to an IRC server using a standard TCP socket. 18 | func ExampleDial() { 19 | conn, err := Dial("irc.quakenet.org:6667") 20 | if err != nil { 21 | log.Fatalln("Could not connect to IRC server") 22 | } 23 | 24 | conn.Close() 25 | } 26 | 27 | // Use NewConn when you want to connect using something else than a standard TCP socket. 28 | // This example first opens an encrypted TLS connection and then uses that to communicate with the server. 29 | func ExampleNewConn() { 30 | tconn, err := tls.Dial("tcp", "irc.quakenet.org:6667", &tls.Config{}) 31 | if err != nil { 32 | log.Fatalln("Could not connect to IRC server") 33 | } 34 | conn := NewConn(tconn) 35 | 36 | conn.Close() 37 | } 38 | 39 | 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" 40 | 41 | var result = [...]*Message{ 42 | { 43 | Command: PING, 44 | Params: []string{"port80a.se.quakenet.org"}, 45 | }, 46 | { 47 | Prefix: &Prefix{ 48 | Name: "port80a.se.quakenet.org", 49 | }, 50 | Command: PONG, 51 | Params: []string{"port80a.se.quakenet.org"}, 52 | Trailing: "port80a.se.quakenet.org", 53 | }, 54 | { 55 | Command: PING, 56 | Params: []string{"chat.freenode.net"}, 57 | }, 58 | { 59 | Prefix: &Prefix{ 60 | Name: "wilhelm.freenode.net", 61 | }, 62 | Command: PONG, 63 | Params: []string{"wilhelm.freenode.net"}, 64 | Trailing: "chat.freenode.net", 65 | }, 66 | } 67 | 68 | func TestDecoder_Decode(t *testing.T) { 69 | 70 | reader := strings.NewReader(stream) 71 | dec := NewDecoder(reader) 72 | 73 | for i, test := range result { 74 | if message, err := dec.Decode(); err != nil { 75 | t.Fatalf("Unexpected error: %s", err.Error()) 76 | } else { 77 | if !reflect.DeepEqual(message, test) { 78 | t.Fatalf("Decoded message looks wrong! (%d)", i) 79 | } 80 | } 81 | } 82 | 83 | if _, err := dec.Decode(); err != io.EOF { 84 | t.Fatal("Decode should return an EOF error!") 85 | } 86 | } 87 | 88 | func TestEncoder_Encode(t *testing.T) { 89 | 90 | buffer := new(bytes.Buffer) 91 | enc := NewEncoder(buffer) 92 | 93 | for _, test := range result { 94 | if err := enc.Encode(test); err != nil { 95 | t.Fatalf("Unexpected error: %s", err.Error()) 96 | } 97 | } 98 | 99 | if buffer.String() != stream { 100 | t.Fatalf("Encoded stream looks wrong!") 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | // +build go1.2 6 | 7 | // Documented in strings_legacy.go 8 | 9 | package irc 10 | 11 | import ( 12 | "strings" 13 | ) 14 | 15 | func indexByte(s string, c byte) int { 16 | return strings.IndexByte(s, c) 17 | } 18 | -------------------------------------------------------------------------------- /strings_legacy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Vic Demuzere 2 | // 3 | // Use of this source code is governed by the MIT license. 4 | 5 | // +build !go1.2 6 | 7 | // Debian Wheezy only ships Go 1.0: 8 | // https://github.com/sorcix/irc/issues/4 9 | // 10 | // This code may be removed when Wheezy is no longer supported. 11 | 12 | package irc 13 | 14 | // indexByte implements strings.IndexByte for Go versions < 1.2. 15 | func indexByte(s string, c byte) int { 16 | for i := range s { 17 | if s[i] == c { 18 | return i 19 | } 20 | } 21 | return -1 22 | } 23 | --------------------------------------------------------------------------------