Documentation ¶
Overview ¶
Package irc provides an IRC client implementation.
This overview provides brief introductions for types and concepts. The godoc for each type contains expanded documentation.
Jump to the package examples to see what writing client code looks like with this package.
The README file links to available extension packages for features such as state tracking, flood protection, and more.
API ¶
These are the main interfaces and structs that you will interact with while using this package:
// A Handler responds to an IRC message. type Handler interface { SpeakIRC(MessageWriter, *Message) } // A MessageWriter can write an IRC message. type MessageWriter interface { WriteMessage(encoding.TextMarshaler) } // Message represents any incoming our outgoing IRC line. type Message struct { // Tags contains any IRCv3 message tags. Tags Tags // Source is where the message originated from. Source Prefix // Command is the IRC verb or numeric (event type) such as PRIVMSG, NOTICE, 001, etc. Command Command // Params contains all the message parameters. Params Params } // A Client manages a connection to an IRC server. type Client struct { //... } // ConnectAndRun starts the client. func (c *Client) ConnectAndRun(ctx context.Context, h Handler) error { //... }
Client ¶
The Client type provides a simple abstraction around an IRC connection. It manages reading and writing messages to the IRC connection and calls your handler for each message it parses. It also deals with protocol concerns like ping timeouts.
Handler ¶
This interface enables the development of handler packages.
type Handler interface { SpeakIRC(MessageWriter, *Message) }
Such packages may implement protocols such as IRCv3 Message IDs, or common bot concerns like preferred/alternate nickname for disconnects, flood protection, and channel state.
Because the Handler interface for the irc package mimics the signature of the http.Handler interface, most patterns for http middleware can also be applied to irc handlers. Search results for phrases like "golang http middleware pattern", "adapter patterns", and "decorator patterns" should all return concepts from tutorials and blog posts that can be applied to this interface.
MessageWriter ¶
The MessageWriter interface accepts any type that knows how to marshal itself into a line of IRC-encoded text.
Most of the time it makes sense to send a Message struct, either by using the NewMessage function or any of the related constructors such as irc.Msg, irc.Notice, irc.Describe, etc.
However, it can also be very simple to implement yourself:
// w is an irc.MessageWriter w.WriteMessage(rawLine("PRIVMSG #World :Hello!")) type rawLine string // MarshalText implements encoding.TextMarshaler func (l rawLine) MarshalText() ([]byte,error) { return []byte(l),nil }
The named Message constructors (irc.Msg, irc.Notice, etc.) should generally be preferred because they explicitly list the available parameters for each command. This provides type safety, ordering safety, and most IDEs will provide intellisense suggestions and documentation for each parameter.
In other words:
// prefer this: w.WriteMessage(irc.Msg("#world", "Hello!")) // instead of this: w.WriteMessage(irc.NewMessage(irc.CmdPrivmsg, "#world", "Hello!")) // and definitely instead of this: w.WriteMessage(rawLine(...))
Router ¶
The Router type is an implementation of Handler. It provides a convenient way to route incoming messages to specific handler functions by matching against message attributes like the command, source, target channel, and much more. It also provides an easy way to apply middleware, either globally or to specific routes. You are not required to use it, however. You can just as easily write your own message handler.
It performs a role comparable to http.ServeMux, though it is not really a multiplexer.
r := &irc.Router{} r.OnText("!watchtime*", handleCommandWatchtime) //... func handleCommandWatchtime(...) { //... }
Middleware ¶
Middleware are just handlers. The term "middleware" applies to handlers which follow a pattern of accepting a handler as one of their arguments and returning a handler.
// logHandler is a function that follows the middleware pattern and implements the Handler interface. func logHandler(next irc.Handler) irc.HandlerFunc { return func(w irc.MessageWriter, m *irc.Message) { log.Printf("parsed: %#v\n", m) next.SpeakIRC(w, m) } } func main() { //... handler := &irc.Router{} //... err := client.ConnectAndRun(..., logHandler(handler)) }
Middleware can intercept outgoing messages by decorating the MessageWriter, as well as call the next handler with a modified *Message. These two abilities allow well-written packages to provide middleware that extend a client with nearly any IRC capability.
Because the ordering of received messages is important for calculating various client states, it is generally not safe for middleware handlers to operate concurrently unless they can maintain message ordering.
Request Lifecycle ¶
To bring it all together, this is the general sequence of events when running a client:
- A Client's ConnectAndRun method is called and given a Handler.
- Internally, the client wraps the provided handler with additional middleware handlers that implement core IRC features.
- ConnectAndRun calls DialFn to connect to an IRC stream.
- The client will begin reading lines from the stream and parse them into Message structs until the connection is closed.
Each Message parsed from the stream will result in a call to the client's handler, which is given a MessageWriter and reference to the parsed Message struct. Assuming that you use the package's Router type as your handler, this is what that sequence looks like:
- The internal client middleware that wrapped your handler will execute on order, calling next.SpeakIRC until they reach the Router (your handler).
- The global middleware attached to the Router, if any, will execute in order.
- The Router will test each route until it finds one where all conditions match.
- If the matched route has any middleware, they will execute in order.
- Finally, the handler provided for the route will execute.
Any of these actions could occur at any point in the chain:
- A handler decides to write a message.
- A handler halts execution by returning without calling the next handler.
- A handler interprets a message to update its own internal state.
- A handler calls the next handler with a modified version of the message.
- A handler calls the next handler with a new MessageWriter that is decorated with a additional functions.
Message Formatting ¶
This package does not implement message formatting. That is to say, there are no irc.Msgf or related functions. Formatting requirements vary widely by application. Some applications will want to extend the formatting rules with their own replacement sequences to include IRC color formatting in replies. Rather than implement nonstandard rules here (and force users to look up replacements), the canonical way to write formatted replies in the style of fmt.Printf is to write your own reply helper functions. For example:
func replyTof(w irc.MessageWriter, m *irc.Message, format string, args ...interface{}) { target,_ := m.Chan() reply := irc.Msg(target, fmt.Sprintf(format, ...args)) w.WriteMessage(reply) }
Example ¶
Hello, #World: The following code connects to an IRC server, waits for RPL_WELCOME, then requests to join a channel called #world, waits for the server to tell us that we've joined, then sends the message "Hello!" to #world, then disconnects with the message "Goodbye.".
package main import ( "context" "log" "github.com/Travis-Britz/irc" ) func main() { bot := &irc.Client{ Addr: "irc.example.com:6697", Nickname: "HelloBot", } r := &irc.Router{} r.OnConnect(func(w irc.MessageWriter, m *irc.Message) { w.WriteMessage(irc.Join("#world")) }) r.OnJoin(func(w irc.MessageWriter, m *irc.Message) { w.WriteMessage(irc.Msg("#world", "Hello!")) w.WriteMessage(irc.Quit("Goodbye.")) }).MatchChan("#world").MatchClient(bot) // run the bot (blocking until exit) err := bot.ConnectAndRun(context.Background(), r) if err != nil { log.Println(err) } }
Output:
Example (Router) ¶
This example uses the message router to perform more complicated message matching with an event callback style. Connects to an IRC server, joins a channel called "#world", sends the message "Hello!", then quits when CTRL+C is pressed.
package main import ( "context" "log" "os" "os/signal" "strings" "github.com/Travis-Britz/irc" ) func main() { ctx, cancel := context.WithCancel(context.Background()) bot := &irc.Client{ Addr: "irc.swiftirc.net:6697", Nickname: "HelloBot", } // Router implements irc.Handler and maps incoming messages (events) to a handler. r := &irc.Router{} r.OnConnect(func(w irc.MessageWriter, m *irc.Message) { w.WriteMessage(irc.Join("#world")) }) // r.OnKick(func(w irc.MessageWriter, m *irc.Message) { // kicked, _ := m.Affected() // if !kicked.Is(bot.Nick().String()) { // return // } // // w.WriteMessage(irc.Msg(e.Nick().String(), "You kicked me!")) // }) r.OnJoin(func(w irc.MessageWriter, m *irc.Message) { w.WriteMessage(irc.Msg("#world", "Hello!")) }). MatchChan("#world"). MatchClient(bot) // When somebody types "!greet nickname" we respond with "Hello, nickname!". r.OnText("!greet &", func(w irc.MessageWriter, m *irc.Message) { text, _ := m.Text() fields := strings.Fields(text) channelName, _ := m.Chan() reply := "Hello, " + fields[1] + "!" // the second field is guaranteed to exist due to the wildcard format w.WriteMessage(irc.Msg(channelName, reply)) }) // Listen for interrupt signals (Ctrl+C) and initiate // a graceful shutdown sequence when one is received. shutdown := make(chan os.Signal) go func() { <-shutdown cancel() }() signal.Notify(shutdown, os.Interrupt) // run the bot (blocking until exit) err := bot.ConnectAndRun(ctx, r) if err != nil { log.Println(err) } }
Output:
Example (Simple) ¶
The simplest possible implementation of a Message handler. In this case, "simple" means it is not using package features. The code should be considered to be a "messy" implementation, but demonstrates how easy it is to get down to the protocol level, if desired.
package main import ( "context" "fmt" "log" "strings" "github.com/Travis-Britz/irc" ) const myName = "HelloBot" // myHandler is an irc.HandlerFunc. // // On connection success (001), it joins #MyChannel. // // On join events, it checks if the joining nickname matched myName and the channel matched #MyChannel // before sending an introduction. // // On privmsg events check if the message target matched our name (indicating a query/DM) and the first // word begins with "Hello" before responding with "hey there!". func myHandler(w irc.MessageWriter, m *irc.Message) { switch m.Command { case "001": w.WriteMessage(rawLine("JOIN #MyChannel")) case "JOIN": if !m.Source.Nick.Is(myName) { return } if !strings.EqualFold("#MyChannel", m.Params.Get(1)) { return } w.WriteMessage(rawLine("PRIVMSG #MyChannel :Hello everybody, my name is " + myName)) case "PRIVMSG": if m.Params.Get(1) == myName { if msgBody := m.Params.Get(2); strings.HasPrefix(msgBody, "Hello") { w.WriteMessage(rawLine(fmt.Sprintf("PRIVMSG %s :hey there!", m.Source.Nick))) } } } } // rawLine is an IRC-formatted message. type rawLine string // MarshalText implements encoding.TextMarshaler, which // is used by irc.MessageWriter. func (l rawLine) MarshalText() ([]byte, error) { return []byte(l), nil } // The simplest possible implementation of a Message handler. // In this case, "simple" means it is not using package features. The code should be // considered to be a "messy" implementation, but demonstrates how easy it is to get // down to the protocol level, if desired. func main() { bot := &irc.Client{ Addr: "irc.example.com:6697", Nickname: myName, } // we need to explicitly convert myHandler to the irc.HandlerFunc type err := bot.ConnectAndRun(context.Background(), irc.HandlerFunc(myHandler)) if err != nil { log.Fatal(err) } }
Output:
Index ¶
- Constants
- type Client
- type Command
- type Handler
- type HandlerFunc
- type Message
- func CTCP(target, command, message string) *Message
- func CTCPReply(target, command, message string) *Message
- func Cap(args ...string) *Message
- func CapEnd() *Message
- func CapLS(version string) *Message
- func CapList() *Message
- func CapReq(cap string) *Message
- func Describe(target, action string) *Message
- func Invite(nick, channel string) *Message
- func Join(channel string) *Message
- func JoinWithKey(channel, key string) *Message
- func Kick(channel, nick string) *Message
- func KickWithReason(channel, nick, reason string) *Message
- func Mode(target, flag, flagParam string) *Message
- func ModeQuery(target string) *Message
- func Msg(target, message string) *Message
- func NewMessage(cmd Command, args ...string) *Message
- func Nick(name string) *Message
- func Notice(target, message string) *Message
- func Part(channel string) *Message
- func PartAll() *Message
- func PartWithReason(channel, reason string) *Message
- func Pass(password string) *Message
- func Ping(message string) *Message
- func Pong(reply string) *Message
- func Quit(message string) *Message
- func TagMsg(tags map[string]string) *Message
- func User(user, realname string) *Message
- type MessageWriter
- type Nickname
- type Params
- type Prefix
- type Router
- func (r *Router) Handle(cmd Command, h Handler) *route
- func (r *Router) HandleFunc(cmd Command, f HandlerFunc) *route
- func (r *Router) OnAction(wildtext string, h HandlerFunc) *route
- func (r *Router) OnCTCP(subcommand string, h HandlerFunc) *route
- func (r *Router) OnCTCPReply(subcommand string, h HandlerFunc) *route
- func (r *Router) OnConnect(h HandlerFunc) *route
- func (r *Router) OnError(h HandlerFunc) *route
- func (r *Router) OnJoin(h HandlerFunc) *route
- func (r *Router) OnNick(h func(nick Nickname, newnick Nickname)) *route
- func (r *Router) OnNotice(wildtext string, h HandlerFunc) *route
- func (r *Router) OnOp(h HandlerFunc) *route
- func (r *Router) OnPart(h HandlerFunc) *route
- func (r *Router) OnQuit(h HandlerFunc) *route
- func (r *Router) OnText(wildtext string, h HandlerFunc) *route
- func (r *Router) OnTextRE(expr string, h HandlerFunc) *route
- func (r *Router) SpeakIRC(mw MessageWriter, m *Message)
- func (r *Router) Use(middlewares ...middleware)
- type Tags
Examples ¶
Constants ¶
const ( CmdAdmin = "ADMIN" // Get information about the administrator of a server. CmdAway = "AWAY" // Set an automatic reply string for any PRIVMSG commands. CmdCap = "CAP" // IRCv3 Capability negotiation. CmdConnect = "CONNECT" // Request a new connection to another server immediately. CmdDie = "DIE" // Shutdown the server. CmdError = "ERROR" // Report a serious or fatal error to a peer. CmdInfo = "INFO" // Get information describing a server. CmdInvite = "INVITE" // Invite a user to a channel. CmdIsOn = "ISON" // Determine if a nickname is currently on IRC. CmdJoin = "JOIN" // Join a channel. CmdKick = "KICK" // Request the forced removal of a user from a channel. CmdKill = "KILL" // Close a client-server connection by the server which has the actual connection. CmdLinks = "LINKS" // List all servernames which are known by the server answering the query. CmdList = "LIST" // List channels and their topics. CmdLUsers = "LUSERS" // Get statistics about the size of the IRC network. CmdMode = "MODE" // User mode. CmdMOTD = "MOTD" // Get the Message of the Day. CmdNames = "NAMES" // List all visible nicknames. CmdNick = "NICK" // ":<newnick>" Define a nickname. CmdNJoin = "NJOIN" // Exchange the list of channel members for each channel between servers. CmdNotice = "NOTICE" // Send a notice message to specific users or channels. CmdOper = "OPER" // Obtain operator privileges. CmdPart = "PART" // Leave a channel. CmdPass = "PASS" // Set a connection password. CmdPing = "PING" // Test for the presence of an active client or server. CmdPong = "PONG" // Reply to a PING message. CmdPrivmsg = "PRIVMSG" // Send private messages between users, as well as to send messages to channels. CmdQuit = "QUIT" // Terminate the client session. CmdRehash = "REHASH" // Force the server to re-read and process its configuration file. CmdRestart = "RESTART" // Force the server to restart itself. CmdServer = "SERVER" // Register a new server. CmdService = "SERVICE" // Register a new service. CmdServList = "SERVLIST" // List services currently connected to the network. CmdSQuery = "SQUERY" // CmdSQuit = "SQUIT" // Break a local or remote server link. CmdStats = "STATS" // Get server statistics. CmdTagMsg = "TAGMSG" // https://ircv3.net/specs/extensions/message-tags.html CmdTime = "TIME" // Get the local time from the specified server. CmdTopic = "TOPIC" // Change or view the topic of a channel. CmdTrace = "TRACE" // Find the route to a server and information about it's peers. CmdUser = "USER" // Specify the username, hostname and realname of a new user. CmdUserHost = "USERHOST" // Get a list of information about upto 5 nicknames. CmdUsers = "USERS" // Get a list of users logged into the server. CmdVersion = "VERSION" // Get the version of the server program. CmdWAllOps = "WALLOPS" // Send a message to all currently connected users who have set the 'w' user mode. CmdWho = "WHO" // List a set of users. CmdWhoIs = "WHOIS" // Get information about a specific user. CmdWhoWas = "WHOWAS" // Get information about a nickname which no longer exists. )
irc commands which may be sent or received by a client.
const ( RplWelcome = "001" // "Welcome to the Internet Relay Network <nick>!<user>@<host>" RplYourHost = "002" // "Your host is <servername>, running version <ver>" RplCreated = "003" // "This server was created <date>" RplMyInfo = "004" // "<servername> <version> <available user modes> <available channel modes>" RplISupport = "005" // http://www.irc.org/tech_docs/005.html http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt https://www.mirc.com/isupport.html RplBounce = "010" // "Try server <server name>, port <port number>" - https://modern.ircdocs.horse/#rplbounce-010 )
irc connection reply codes.
const ( RplTraceLink = "200" // "Link <version & debug level> <destination><next server> V<protocol version> <link uptime in seconds><backstream sendq> <upstream sendq>" RplTraceConnecting = "201" // "Try. <class> <server>" RplTraceHandshake = "202" // "H.S. <class> <server>" RplTraceUnknown = "203" // "???? <class> [<client IP address in dot form>]" RplTraceOperator = "204" // "Oper <class> <nick>" RplTraceUser = "205" // "User <class> <nick>" RplTraceServer = "206" // "Serv <class> <int>S <int>C <server><nick!user|*!*>@<host|server> V<protocol version>" RplTraceService = "207" // "Service <class> <name> <type> <activetype>" RplTraceNewtype = "208" // "<newtype> 0 <client name>" RplTraceClass = "209" // "Class <class> <count>" RplTraceReconnect = "210" // Unused. RplStatsLinkInfo = "211" // "<linkname> <sendq> <sent messages> <sentKbytes> <received messages> <received Kbytes> <timeopen>" RplStatsCommands = "212" // "<command> <count> <byte count> <remotecount>" RplEndOfStats = "219" // "<stats letter> :End of STATS report" RplUModeIs = "221" // "<user mode string>" RplServList = "234" // "<name> <server> <mask> <type><hopcount> <info>" RplServListEnd = "235" // "<mask> <type> :End of service listing" RplStatsUptime = "242" // ":Server Up %d days %d:%02d:%02d" RplStatsOLine = "243" // "O <hostmask> * <name>" RplLUserClient = "251" // ":There are <integer> users and <integer> services on<integer> servers" RplLUserOp = "252" // "<integer> :operator(s) online" RplLUserUknownL = "253" // "<integer> :unknown connection(s)" RplLUserChannels = "254" // "<integer> :channels formed" RplLUserMe = "255" // ":I have <integer> clients and <integer> servers" RplAdminMe = "256" // "<server> :Administrative info" RplAdminLoc1 = "257" // ":<admin info>" RplAdminLoc2 = "258" // ":<admin info>" RplAdminEmail = "259" // ":<admin info>" RplTraceLog = "261" // "File <logfile> <debug level>" RplTraceEnd = "262" // "<server name> <version & debug level> :End of TRACE" RplTryAgain = "263" // "<command> :Please wait a while and try again." RplAway = "301" // "<nick> :<away message>" RplUserHost = "302" // ":*1<reply> *( " " <reply> )" RplIsOn = "303" // ":*1<nick> *( " " <nick> )" RplUnAway = "305" // ":You are no longer marked as being away" RplNowAway = "306" // ":You have been marked as being away" RplWhoIsUser = "311" // "<nick> <user> <host> * :<real name>" RplWhoIsServer = "312" // "<nick> <server> :<server info>" RplWhoIsOperator = "313" // "<nick> :is an IRC operator" RplWhoWasUser = "314" // "<nick> <user> <host> * :<real name>" RplEndOfWho = "315" // "<name> :End of WHO list" RplWhoIsIdle = "317" // "<nick> <integer> :seconds idle" RplEndOfWhoIs = "318" // "<nick> :End of WHOIS list" RplWhoIsChannels = "319" // "<nick> :*( ( "@" / "+" ) <channel>" " )" RplListStart = "321" // Obsolete. RplList = "322" // "<channel> <# visible> :<topic>" RplListEnd = "323" // ":End of LIST" RplChannelModeIs = "324" // "<channel> <mode> <mode params>" RplUniqOpIs = "325" // "<channel> <nickname>" RplNoTopic = "331" // "<channel> :No topic is set" RplTopic = "332" // "<channel> :<topic>" RplWhoisBot = "335" // "<nick> <target> :<message>" RplInviting = "341" // "<channel> <nick>" RplSummoning = "342" // "<user> :Summoning user to IRC" RplInviteList = "346" // "<channel> <invitemask>" RplEndOfInviteList = "347" // "<channel> :End of channel invite list" RplExceptList = "348" // "<channel> <exceptionmask>" RplEndOfExceptList = "349" // "<channel> :End of channel exception list" RplVersion = "351" // "<version>.<debuglevel> <server>:<comments>" RplWhoReply = "352" // "<channel> <user> <host> <server><nick> ( "H" / "G" > ["*"] [ ("@" / "+" ) ] :<hopcount> <real name>" RplNamReply = "353" // "( "=" / "*" / "@" ) <channel>:[ "@" / "+" ] <nick> *( " " ["@" / "+" ] <nick> )" RplLinks = "364" // "<mask> <server> :<hopcount> <serverinfo>" RplEndOfLinks = "365" // "<mask> :End of LINKS list" RplEndOfNames = "366" // "<channel> :End of NAMES list" RplBanList = "367" // "<channel> <banmask>" RplEndOfBanList = "368" // "<channel> :End of channel ban list" RplEndOfWhoWas = "369" // "<nick> :End of WHOWAS" RplInfo = "371" // ":<string>" RplMOTD = "372" // ":- <text>" RplEndOfInfo = "374" // ":End of INFO list" RplMOTDStart = "375" // ":- <server> Message of the day - " RplEndOfMOTD = "376" // ":End of MOTD command" RplYoureOper = "381" // ":You are now an IRC operator" RplRehashing = "382" // "<config file> :Rehashing" RplYoureService = "383" // "You are service <servicename>" RplTime = "391" // "<server> :<string showing server's local time>" RplUsersStart = "392" // ":UserID Terminal Host" RplUsers = "393" // ":<username> <ttyline> <hostname>" RplEndOfUsers = "394" // ":End of users" RplNoUsers = "395" // ":Nobody logged in" RplHostHidden = "396" // "fubarbot <host> :is now your displayed host" Reply to a user when user mode +x (host masking) was set successfully https://www.alien.net.au/irc/irc2numerics.html )
irc command reply codes.
const ( RplErrNoSuchNick = "401" // "<nickname> :No such nick/channel" RplErrNoSuchServer = "402" // "<server name> :No such server" RplErrNoSuchChannel = "403" // "<channel name> :No such channel" RplErrCannotSendToChan = "404" // "<channel name> :Cannot send to channel" RplErrTooManyChannels = "405" // "<channel name> :You have joined too many channels" RplErrWasNoSuchNick = "406" // "<nickname> :There was no such nickname" RplErrTooManyTargets = "407" // "<target> :<error code> recipients. <abortmessage>" RplErrNoSuchService = "408" // "<service name> :No such service" RplErrNoOrigin = "409" // ":No origin specified" RplErrInvalidCapCmd = "410" RplErrNoRecipient = "411" // ":No recipient given (<command>)" RplErrNoTextToSend = "412" // ":No text to send" RplErrNoToplevel = "413" // "<mask> :No toplevel domain specified" RplErrWildToplevel = "414" // "<mask> :Wildcard in toplevel domain" RplErrBadMask = "415" // "<mask> :Bad Server/host mask" RplErrUnknownCommand = "421" // "<command> :Unknown command" RplErrNoMOTD = "422" // ":MOTD File is missing" RplErrNoAdminInfo = "423" // "<server> :No administrative info available" RplErrFileError = "424" // ":File error doing <file op> on <file>" RplErrNoNicknameGiven = "431" // ":No nickname given" RplErrErroneousNickname = "432" // "<client> <nick> :Erroneus nickname" RplErrNicknameInUse = "433" // "<client> <nick> :Nickname is already in use" RplErrNickCollision = "436" // "<nick> :Nickname collision KILL from<user>@<host>" RplErrUserNotInChannel = "441" // "<nick> <channel> :They aren't on that channel" RplErrNotOnChannel = "442" // "<channel> :You're not on that channel" RplErrUserOnChannel = "443" // "<user> <channel> :is already on channel" RplErrNoLogin = "444" // "<user> :User not logged in" RplErrSummonDisabled = "445" // ":SUMMON has been disabled" RplErrUsersDisabled = "446" // ":USERS has been disabled" RplErrNotRegistered = "451" // ":You have not registered" RplErrNeedMoreParams = "461" // "<command> :Not enough parameters" RplErrAlreadyRegistered = "462" // ":Unauthorized command (already registered)" RplErrNoPermForHost = "463" // ":Your host isn't among the privileged" RplErrPasswdMismatch = "464" // ":Password incorrect" RplErrYoureBannedCreep = "465" // ":You are banned from this server" RplErrYouWillBeBanned = "466" // RplErrKeySet = "467" // "<channel> :Channel key already set" RplErrChannelIsFull = "471" // "<channel> :Cannot join channel (+l)" RplErrUnknownMode = "472" // "<char> :is unknown mode char to me for <channel>" RplErrInviteOnlyChan = "473" // "<channel> :Cannot join channel (+i)" RplErrBannedFromChan = "474" // "<channel> :Cannot join channel (+b)" RplErrBadChannelKey = "475" // "<channel> :Cannot join channel (+k)" RplErrBadChanMask = "476" // "<channel> :Bad Channel Mask" RplErrNoChanModes = "477" // "<channel> :Channel doesn't support modes" RplErrBanListFull = "478" // "<channel> <char> :Channel list is full" RplErrNoPrivileges = "481" // ":Permission Denied- You're not an IRC operator" RplErrChanOPrivsNeeded = "482" // "<channel> :You're not channel operator" RplErrCantKillServer = "483" // ":You can't kill a server!" RplErrRestricted = "484" // ":Your connection is restricted!" RplErrUniqOPrivsNeeded = "485" // ":You're not the original channel operator" RplErrNoOperHost = "491" // ":No O-lines for your host" RplErrUModeUnknownFlag = "501" // ":Unknown MODE flag" RplErrUsersDontMatch = "502" // ":Cannot change mode for other users" )
irc error reply codes.
const ( CTCPAction = "_CTCP_QUERY_ACTION" CTCPVersionQuery = "_CTCP_QUERY_VERSION" CTCPVersionReply = "_CTCP_REPLY_ACTION" CTCPClientInfoQuery = "_CTCP_QUERY_CLIENTINFO" CTCPClientInfoReply = "_CTCP_REPLY_CLIENTINFO" CTCPPingQuery = "_CTCP_QUERY_PING" CTCPPingReply = "_CTCP_REPLY_PING" CTCPTimeQuery = "_CTCP_QUERY_TIME" CTCPTimeReply = "_CTCP_REPLY_TIME" )
Client-to-Client Protocol command constants. These commands are NOT sent by the server; they are instead generated internally as replacements for CTCP-formatted PRIVMSG and NOTICE messages.
For convenience, these constants are defined for well-known CTCP messages. To create handlers that match arbitrary CTCP commands and replies, see NewCTCPCmd and NewCTCPReplyCmd.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct { // The address ("host:port") of the IRC server. Only TLS connections are supported; use DialFn for anything else. // Addr is only used when DialFn is nil. Addr string // The nickname used by the Client when connecting to an IRC network (required). // Nicknames cannot contain spaces. Nickname string // The user name (required). // User cannot contain spaces. User string // The realname of the client (required). // Also referred to as the gecos field. // Realname may contain spaces Realname string // The connection password (optional: depends on the network). Pass string // DialFn is a function that accepts no parameters and returns an io.ReadWriteCloser and error. // // The returned connection can be any io.ReadWriteCloser: irc, ircs, ws, wss, a server mock, etc. // The only requirement is that the stream consists of CRLF-delimited IRC messages. // // When DialFn is nil, the default behavior dials Addr with tls.Dial. DialFn func() (io.ReadWriteCloser, error) // ErrorLog specifies an optional logger for errors returned from parsing and encoding messages. // If nil, logging is done via the log package's standard logger. ErrorLog *log.Logger // contains filtered or unexported fields }
A Client manages a connection to an IRC server. It reads/writes IRC lines on the connection, and calls the handler for each Message it parses from the connection.
Example (DialFn) ¶
package main import ( "io" "net" "github.com/Travis-Britz/irc" ) func main() { client := &irc.Client{Nickname: "WiZ"} client.DialFn = func() (io.ReadWriteCloser, error) { return net.Dial("tcp", "irc.example.com:6667") } }
Output:
Example (DialFnDecorated) ¶
package main import ( "io" "net" "os" "github.com/Travis-Britz/irc" "github.com/Travis-Britz/irc/ircdebug" ) func main() { client := &irc.Client{Nickname: "WiZ"} client.DialFn = func() (io.ReadWriteCloser, error) { conn, err := net.Dial("tcp", "irc.example.com:6667") return ircdebug.WriteTo(os.Stdout, conn, "-> ", "<- "), err } }
Output:
func (*Client) ConnectAndRun ¶
ConnectAndRun establishes a connection to the remote IRC server and sends the appropriate IRC protocol commands to begin the connection and capability negotiation.
The Handler h is called for every incoming Message parsed from the connection. Handlers are called synchronously because the ordering of incoming messages matters.
ConnectAndRun always returns an error, with one exception: if the client sends an IRC "QUIT" message followed by receiving an io.EOF from the connection, then the returned error will be nil.
Example (Reconnect) ¶
This example deals with client disconnects. It runs the connect loop for a client in a goroutine, doubling the time between reconnect attempts each time the client exits with an error.
package main import ( "context" "log" "sync" "time" "github.com/Travis-Britz/irc" ) func main() { client := &irc.Client{Nickname: "HelloBot"} connected := make(chan bool, 1) handler := &irc.Router{} handler.OnConnect(func(w irc.MessageWriter, m *irc.Message) { select { case connected <- true: default: } w.WriteMessage(irc.Join("#World")) }) ctx := context.Background() wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() var delay time.Duration for { select { case <-ctx.Done(): return case <-connected: delay = 0 case <-time.After(delay): log.Println("connecting...") err := client.ConnectAndRun(ctx, handler) log.Println("connection ended:", err) select { case <-ctx.Done(): // after a connection ends, make sure we're not supposed to exit before looping around again. return default: if err != nil { delay = delay*2 + time.Second } log.Println("reconnect delay:", delay) } } } }() wg.Wait() }
Output:
func (*Client) Nick ¶
Nick returns the client's current nickname according to the client's internal state tracking. This is used by some route matchers to determine when a message originated from or targeted our client.
func (*Client) WriteMessage ¶
func (c *Client) WriteMessage(m encoding.TextMarshaler)
WriteMessage implements irc.MessageWriter. It writes m to the client's connection. Marshaling errors will be reported to the client's logger. Write errors will cause the client's run method to return with the first error.
type Command ¶
type Command string
Command is an IRC command such as PRIVMSG, NOTICE, 001, etc.
A command may also be known as the "verb", "event type", or "numeric".
func NewCTCPCmd ¶
NewCTCPCmd returns a Command which will match the internal representation of a CTCP-encoded PRIVMSG, for mapping CTCP messages to handlers.
The returned Command is *not* a valid IRC commnad. For sending CTCP-formatted messages, see func CTCP. todo: rename this and newctcpreplycmd so it doesn't get confused with the Message builder?
func NewCTCPReplyCmd ¶
NewCTCPReplyCmd returns a Command which will match the internal representation of a CTCP-encoded NOTICE, for mapping CTCP replies to handlers.
The returned Command is *not* a valid IRC command. For sending CTCP-formatted replies, see func CTCPReply.
type Handler ¶
type Handler interface {
SpeakIRC(MessageWriter, *Message)
}
A Handler responds to an IRC message.
An IRC message may be any type, including PRIVMSG, NOTICE, JOIN, Numerics, etc. It is up to the calling function to map incoming messages/commands to the appropriate handler.
Handlers should avoid modifying the provided Message.
type HandlerFunc ¶
type HandlerFunc func(MessageWriter, *Message)
The HandlerFunc type is an adapter to allow the usage of ordinary functions as handlers, following the same pattern as http.HandlerFunc.
func (HandlerFunc) SpeakIRC ¶
func (f HandlerFunc) SpeakIRC(w MessageWriter, m *Message)
SpeakIRC calls f(w, m).
type Message ¶
type Message struct { // Tags contains IRCv3 message tags. // Tags are included by the server if the message-tags capability has been negotiated. Tags Tags // Source is where the message originated from. // It's set by the prefix portion of an IRC message. // // Source should be left empty for messages that will be written to an IRC connection. // See the docs for [Message.MarshalText] for more details. Source Prefix // Command is the IRC verb or numeric such as PRIVMSG, NOTICE, 001, etc. // It may also sometimes be referred to as the event type. Command Command // Params contains all the message parameters. // If a message included a trailing component, // it will be included without special treatment. // For outgoing messages, // only the last parameter may contain a SPACE (ascii 32). // Including a space in any other parameter will result in undefined behavior. Params Params // contains filtered or unexported fields }
Message represents any incoming or outgoing IRC line.
Background ¶
IRC is a line-delimited, text-based protocol consisting of incoming and outgoing messages. The terms "message", "line", or "event" might be used within this package to refer to a Message (although "event" usually only refers to an incoming message).
A message consists of four parts: tags, prefix, verb, and params.
func CTCP ¶
CTCP constructs a CTCP (Client-to-Client Protocol) encoded message to the target. command is the CTCP subcommand.
func CTCPReply ¶
CTCPReply constructs a message encoded in the CTCP reply format. target should be the nickname that sent us a CTCP message, command is the subcommand that was sent to us, and message depends on the type of query.
func Cap ¶
Cap sends a CAP command as part of capability negotiation. args are the subcommand and parameters of the CAP command.
func CapLS ¶
CapLS requests a list of the capabilities supported by the server.
version is the capability negotiation protocol version, e.g. "302" for version 3.2.
func CapList ¶
func CapList() *Message
CapList requests a list of the capabilities which have been negotiated and enabled for the client's connection.
func Describe ¶
Describe constructs a new Message of type CTCP ACTION, with target being the intended target channel or nickname, and message being the text body.
Describe is equivalent to the "/me" or "/describe" commands that one might enter into the text input field of popular IRC clients.
By convention, actions are written in third-person.
Actions are often displayed with different formatting from regular messages. It is common for clients to display actions with italicised text and use a different color, and sometimes prefix the message with an asterisk followed by the user's nickname. The specific display formatting varies depending on which client program each user is connecting with.
For example, compare an action message with a regular privmsg:
Describe("#foo", "slaps Bob around a bit with a large trout") Msg("#foo", "take that!")
is equivalent to typing
/me slaps Bob around a bit with a large trout take that!
in channel #foo on most IRC clients, and might be displayed by a receiving client as
- Alice slaps Bob around a bit with a large trout <Alice> take that!
but with italics and possibly colorized.
func JoinWithKey ¶
JoinWithKey constructs a channel join command for channels that require a key (channel mode +k is set).
func KickWithReason ¶
KickWithReason is similar to Kick, but the kick message will display reason.
func Msg ¶
Msg constructs a new Message of type PRIVMSG, with target being the intended target channel or nickname, and message being the text body.
func NewMessage ¶
NewMessage constructs a new Message to be sent on the connection with cmd as the verb and args as the message parameters.
Only the last argument may contain SPACE (ascii 32, %x20). This is a limitation defined in the IRC protocol. Including SPACE in any other argument will result in undefined behavior.
It is common to use '*' in place of an unused parameter. This has the benefit of matching all cases in situations where a wildcard match is allowed.
Example (AttachingTags) ¶
The Message returned by NewMessage does not have any tags set. This also includes the Message returned by the Msg, Notice, and other related functions.
To attach tags for an outgoing message, simply access the Tags field and call the Set method before passing the message to a MessageWriter.
package main import ( "github.com/Travis-Britz/irc" ) func main() { // h := irc.HandlerFunc(func(w irc.MessageWriter, m *irc.Message) { response := irc.Msg("#somechannel", "hello!") response.Tags.Set("msgid", "63E1033A051D4B41B1AB1FA3CF4B243E") // w.WriteMessage(response) // }) }
Output:
func Notice ¶
Notice constructs a new message of type NOTICE, with target being the intended target channel or nickname, and message being the text body.
func PartWithReason ¶
PartWithReason is the same as Part, but with a message that may be shown to other clients
func Ping ¶
Ping constructs a command to PING the connection. The server will typically respond with PONG <message>, although it is possible on some networks to ping a specific server, in which case the original message is not returned.
Ping is not the same as a CTCP ping, which is sent to a client or channel via a PRIVMSG command instead. To build a CTCP ping, use CTCP(<target>, "PING", time.Now()). Replies will match a Message of type CTCPReply(<yournick>, "PING", <sent timestamp>).
func Pong ¶
Pong builds the reply to a PING from the connection. The reply message must be the same as the original PING message.
func Quit ¶
Quit constructs a command that will cause the server to terminate the client's connection, and may display the quit message to clients that are configured to show quit messages.
func User ¶
User is used at the beginning of a connection to specify the username and realname of a new user.
realname may contain spaces.
func (*Message) Chan ¶
Chan returns the channel a message applies to. In the case of query messages, Chan will return an empty string. If the message target was a channel name prefixed with membership prefixes ('@', '+', etc.) the prefixes will be stripped.
func (*Message) IncludePrefix ¶
func (m *Message) IncludePrefix()
IncludePrefix controls whether the Source field will be marshaled by MarshalText. todo: wording The Source field will be included in the encoded text for the sake of compatibility with encoding.TextUnmarshaler. However, the Source field should be left empty for messages which are written to an IRC connection. This is because [RFC 1459] states that for messages originating from a client, it is invalid to include any prefix other than the client's nickname. The RFC also instructs servers to silently discard messages which do not follow this rule.
[RFC 1459]: https://datatracker.ietf.org/doc/html/rfc1459#section-2.3 todo: rename method The default is to enable this setting for received messages and disable it for new messages. Generally this should not be needed except in the case of middleware cloning a message and passing the copy to the next handler.
func (*Message) MarshalText ¶
MarshalText implements encoding.TextMarshaler, mainly for use with irc.MessageWriter.
func (*Message) Target ¶
Target is the target of the message, which will be the current nickname of the client in the case of direct messages (queries), or the channel name if sent to a channel, or a prefix followed by the channel name if sent to a specific group of users in a channel, e.g. "+#foo" for all users on a channel with +v or higher.
func (*Message) Text ¶
Text returns the free-form text portion of a message for the well-known (named) IRC commands. An error is returned if the method is called for unsupported message types. If err is not nil, then Text will contain the entire parameter list joined together as one string. However, for commands that return an error, it may be better to call Params.Get directly.
Supported commands include PRIVMSG, NOTICE, PART, QUIT, ERROR, and more.
In the case of PART and KICK, Text contains the <reason> message parameter.
The error may be discarded without checking If it's known that the message will always be a supported command, for example when used inside a handler that is only ever called for PRIVMSG events, then it is safe to discard err. Errors are only returned to prevent the method from returning unexpected results to callers that assume it will work for all message types.
func (*Message) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler, accepting a line read from an IRC stream. text should not include the trailing CR-LF pair.
This will unmarshal an arbitrarily long sequence of bytes. Length limitations should be implemented at the scanner.
type MessageWriter ¶
type MessageWriter interface { // WriteMessage writes the message to the client's outgoing message queue. // The given encoding.TextMarshaler MUST return a byte slice which conforms to the IRC protocol. // If the slice does not end in "\r\n", then the sequence will be appended. // // The returned slice from the MarshalText method will be written to the connection with a single call to Write. // If a type implements message splitting for long messages, // then the entire slice must consist of multiple valid "\r\n"-delimited IRC messages. // // For example: // "PRIVMSG #foo :supercalifragilisticexpi-\r\nPRIVMSG #foo :alidocious\r\n" // // It is the responsibility of the MarshalText method implementer to ensure that messages are formatted correctly, // and in the case of custom message splitting and continuation, // that flood limits are not reached. WriteMessage(encoding.TextMarshaler) }
MessageWriter contains methods for sending IRC messages to a server.
type Nickname ¶
type Nickname string
type Params ¶
type Params []string
Params contains the slice of arguments for a message.
Prefer the Get method for reading params rather than accessing the slice directly.
For outgoing messages, only the last parameter may contain SPACE (ascii 32). Including SPACE in any other parameter will result in undefined behavior.
If a message included a trailing component as defined in RFC 1459, it will be included as a normal parameter.
Example (Get) ¶
This example demonstrates why using the Get method of a Params type is preferable to accessing its slice index directly. Note the parsing behavior around missing and empty params. The parser only interprets syntax without understanding the semantics of a PART command. In other words, it does not know how many parameters a PART command has. Similarly, functions which interpret a PART command don't care about the protocol syntax difference between omitting a parameter or leaving it empty: in both cases they would only care about checking if the second param is equal to empty string.
package main import ( "fmt" "log" "github.com/Travis-Britz/irc" ) func main() { lines := []struct { raw string description string }{{ raw: ":WiZ PART #foo", description: "PART with omitted reason", }, { raw: ":WiZ PART #foo :", description: "PART with empty reason", }, { raw: ":WiZ PART #foo :leaving now", description: `PART with reason "leaving now"`, }, } m := &irc.Message{} for _, line := range lines { err := m.UnmarshalText([]byte(line.raw)) if err != nil { log.Println(err) } fmt.Printf("%s:\n", line.description) fmt.Printf("parsed: %#v\n", m.Params) fmt.Printf("get 1,2: %q, %q\n", m.Params.Get(1), m.Params.Get(2)) } }
Output: PART with omitted reason: parsed: irc.Params{"#foo"} get 1,2: "#foo", "" PART with empty reason: parsed: irc.Params{"#foo", ""} get 1,2: "#foo", "" PART with reason "leaving now": parsed: irc.Params{"#foo", "leaving now"} get 1,2: "#foo", "leaving now"
func (Params) Get ¶
Get returns the nth parameter (starting at 1) from the parameters list, or "" (empty string) if it did not exist.
Because parameters have meaning based on their position in the argument list, and because the meaning and position depends on which command/verb was used, Get does not differentiate between missing and empty parameters. Callers do not need to worry whether a parameter exists or not; they may simply check whether ordinal parameter n is equivalent to empty string. todo: translate that paragraph to english.
type Prefix ¶
Prefix is the optional message (line) prefix, which indicates the source (user or server) of the message, depending on the prefix format.
Example line with no prefix:
PING :86F3E357
Example nickname-only prefix:
:Travis MODE Travis :+ixz
Example "fulladdress" prefix:
:NickServ!services@services.host NOTICE Travis :This nickname is registered...
Example server prefix:
:fiery.ca.us.SwiftIRC.net MODE #foo +nt
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router provides a Handler which can match incoming messages against a slice of route handlers. Matching is based on message attributes such as the command (verb), source, message contents, and more.
Routes are currently tested in the order they were added, and only the first matching route's handler will be called. However, this behavior may change in the future to allow for more efficient route matching. Therefore, care should be taken to avoid adding multiple routes which may trigger on the same input message.
func (*Router) HandleFunc ¶
func (r *Router) HandleFunc(cmd Command, f HandlerFunc) *route
HandleFunc appends f to the list of handlers for cmd.
func (*Router) OnAction ¶
func (r *Router) OnAction(wildtext string, h HandlerFunc) *route
OnAction attaches a handler for PRIVMSG that matches CTCP ACTION, and follows the same format as OnText.
func (*Router) OnCTCP ¶
func (r *Router) OnCTCP(subcommand string, h HandlerFunc) *route
OnCTCP attaches a route handler that matches against a CTCP message of type subcommand.
func (*Router) OnCTCPReply ¶
func (r *Router) OnCTCPReply(subcommand string, h HandlerFunc) *route
OnCTCPReply attaches a route handler that matches against a CTCP Reply of type subcommand.
func (*Router) OnConnect ¶
func (r *Router) OnConnect(h HandlerFunc) *route
OnConnect attaches a handler which is called upon successful connection to an IRC server, after capability negotiation is complete (on servers which support capability negotiation). More specifically, it is triggered by numeric 001 (RPL_WELCOME).
func (*Router) OnError ¶
func (r *Router) OnError(h HandlerFunc) *route
OnError is triggered when the server sends an ERROR message, usually on disconnect.
func (*Router) OnJoin ¶
func (r *Router) OnJoin(h HandlerFunc) *route
OnJoin attaches a handler for JOIN events.
func (*Router) OnNotice ¶
func (r *Router) OnNotice(wildtext string, h HandlerFunc) *route
OnNotice is triggered when a NOTICE is received from a client on the server, following the same format as OnText. For server notices, use MatchServer.
func (*Router) OnOp ¶
func (r *Router) OnOp(h HandlerFunc) *route
func (*Router) OnPart ¶
func (r *Router) OnPart(h HandlerFunc) *route
OnPart is triggered when a client departs a channel we are on.
func (*Router) OnQuit ¶
func (r *Router) OnQuit(h HandlerFunc) *route
OnQuit is triggered when a client which shares a channel with us disconnects from the server.
func (*Router) OnText ¶
func (r *Router) OnText(wildtext string, h HandlerFunc) *route
OnText attaches a handler for PRIVMSG events that match text. text is a wildcard string:
- matches any text & matches any word ? matches a single character text matches if exact match text* matches if text starts with word *text matches if text ends with word *text* matches if text is anywhere
func (*Router) OnTextRE ¶
func (r *Router) OnTextRE(expr string, h HandlerFunc) *route
OnTextRE attaches the handler h for PRIVMSG events that match the Go regular expression expr.
func (*Router) SpeakIRC ¶
func (r *Router) SpeakIRC(mw MessageWriter, m *Message)
SpeakIRC implements Handler
func (*Router) Use ¶
func (r *Router) Use(middlewares ...middleware)
Use appends global middleware to the router. Middleware are functions which accept a handler and return a handler.
Global middleware are run against every incoming line, even if there were no matching routes for the message.
Middleware can do many things:
- Mutate incoming messages before passing them to the next Handler
- Decorate the MessageWriter with additional functionality before passing it to the next Handler
- Write messages to the MessageWriter
- Prevent additional processing by not calling the next Handler
These are very powerful abilities, but it is very easy to use them improperly.
Middleware will execute in the order they were attached.
type Tags ¶
Tags represents the IRCv3 message tags for an incoming or outgoing IRC line.
func (Tags) Get ¶
Get will get the message tag value for key. All variations of missing or empty values return an empty string. To check whether a message included a specific tag key, use Has.