// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use // of this source code is governed by the MIT license that can be found in // the LICENSE file. package girc import ( "bytes" "fmt" "strings" "time" ) const ( eventSpace byte = ' ' // Separator. maxLength = 510 // Maximum length is 510 (2 for line endings). ) // cutCRFunc is used to trim CR characters from prefixes/messages. func cutCRFunc(r rune) bool { return r == '\r' || r == '\n' } // Event represents an IRC protocol message, see RFC1459 section 2.3.1 // // <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf> // <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>] // <command> :: <letter>{<letter>} | <number> <number> <number> // <SPACE> :: ' '{' '} // <params> :: <SPACE> [':' <trailing> | <middle> <params>] // <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL // or CR or LF, the first of which may not be ':'> // <trailing> :: <Any, possibly empty, sequence of octets not including NUL or // CR or LF> // <crlf> :: CR LF type Event struct { // Source is the origin of the event. Source *Source `json:"source"` // Tags are the IRCv3 style message tags for the given event. Only use // if network supported. Tags Tags `json:"tags"` // Timestamp is the time the event was received. This could optionally be // used for client-stored sent messages too. If the server supports the // "server-time" capability, this is synced to the UTC time that the server // specifies. Timestamp time.Time `json:"timestamp"` // Command that represents the event, e.g. JOIN, PRIVMSG, KILL. Command string `json:"command"` // Params (parameters/args) to the command. Commonly nickname, channel, etc. Params []string `json:"params"` // Trailing text. e.g. with a PRIVMSG, this is the message text (part // after the colon.) Trailing string `json:"trailing"` // EmptyTrailing, if true, the text prefix (:) will be added even if // Event.Trailing is empty. EmptyTrailing bool `json:"empty_trailing"` // Sensitive should be true if the message is sensitive (e.g. and should // not be logged/shown in debugging output). Sensitive bool `json:"sensitive"` // If the event is an echo-message response. Echo bool `json:"echo"` } // ParseEvent takes a string and attempts to create a Event struct. Returns // nil if the Event is invalid. func ParseEvent(raw string) (e *Event) { // Ignore empty events. if raw = strings.TrimFunc(raw, cutCRFunc); len(raw) < 2 { return nil } var i, j int e = &Event{Timestamp: time.Now()} if raw[0] == prefixTag { // Tags end with a space. i = strings.IndexByte(raw, eventSpace) if i < 2 { return nil } e.Tags = ParseTags(raw[1:i]) if rawServerTime, ok := e.Tags.Get("time"); ok { // Attempt to parse server-time. If we can't parse it, we just // fall back to the time we received the message (locally.) if stime, err := time.Parse(capServerTimeFormat, rawServerTime); err == nil { e.Timestamp = stime.Local() } } raw = raw[i+1:] } if raw[0] == messagePrefix { // Prefix ends with a space. i = strings.IndexByte(raw, eventSpace) // Prefix string must not be empty if the indicator is present. if i < 2 { return nil } e.Source = ParseSource(raw[1:i]) // Skip space at the end of the prefix. i++ } // Find end of command. j = i + strings.IndexByte(raw[i:], eventSpace) if j < i { // If there are no proceeding spaces, it's the only thing specified. e.Command = strings.ToUpper(raw[i:]) return e } e.Command = strings.ToUpper(raw[i:j]) // Skip the space after the command. j++ // Check if and where the trailing text is within the incoming line. var lastIndex, trailerIndex int for { // We must loop through, as it's possible that the first message // prefix is not actually what we want. (e.g, colons are commonly // used within ISUPPORT to delegate things like CHANLIMIT or TARGMAX.) lastIndex = trailerIndex trailerIndex = strings.IndexByte(raw[j+lastIndex:], messagePrefix) if trailerIndex == -1 { // No trailing argument found, assume the rest is just params. e.Params = strings.Split(raw[j:], string(eventSpace)) return e } // This means we found a prefix that was proceeded by a space, and // it's good to assume this is the start of trailing text to the line. if raw[j+lastIndex+trailerIndex-1] == eventSpace { i = lastIndex + trailerIndex break } // Keep looping through until we either can't find any more prefixes, // or we find the one we want. trailerIndex += lastIndex + 1 } // Set i to that of the substring we were using before, and where the // trailing prefix is. i = j + i // Check if we need to parse arguments. If so, take everything after the // command, and right before the trailing prefix, and cut it up. if i > j { e.Params = strings.Split(raw[j:i-1], string(eventSpace)) } e.Trailing = raw[i+1:] // We need to re-encode the trailing argument even if it was empty. if len(e.Trailing) <= 0 { e.EmptyTrailing = true } return e } // Copy makes a deep copy of a given event, for use with allowing untrusted // functions/handlers edit the event without causing potential issues with // other handlers. func (e *Event) Copy() *Event { if e == nil { return nil } newEvent := &Event{ Timestamp: e.Timestamp, Command: e.Command, Trailing: e.Trailing, EmptyTrailing: e.EmptyTrailing, Sensitive: e.Sensitive, Echo: e.Echo, } // Copy Source field, as it's a pointer and needs to be dereferenced. if e.Source != nil { newEvent.Source = e.Source.Copy() } // Copy Params in order to dereference as well. if e.Params != nil { newEvent.Params = make([]string, len(e.Params)) copy(newEvent.Params, e.Params) } // Copy tags as necessary. if e.Tags != nil { newEvent.Tags = Tags{} for k, v := range e.Tags { newEvent.Tags[k] = v } } return newEvent } // Equals compares two Events for equality. func (e *Event) Equals(ev *Event) bool { if e.Command != ev.Command || e.Trailing != ev.Trailing || len(e.Params) != len(ev.Params) { return false } for i := 0; i < len(e.Params); i++ { if e.Params[i] != ev.Params[i] { return false } } if !e.Source.Equals(ev.Source) || !e.Tags.Equals(ev.Tags) { return false } return true } // Len calculates the length of the string representation of event. Note that // this will return the true length (even if longer than what IRC supports), // which may be useful if you are trying to check and see if a message is // too long, to trim it down yourself. func (e *Event) Len() (length int) { if e.Tags != nil { // Include tags and trailing space. length = e.Tags.Len() + 1 } if e.Source != nil { // Include prefix and trailing space. length += e.Source.Len() + 2 } length += len(e.Command) if len(e.Params) > 0 { length += len(e.Params) for i := 0; i < len(e.Params); i++ { length += len(e.Params[i]) } } if len(e.Trailing) > 0 || e.EmptyTrailing { // Include prefix and space. length += len(e.Trailing) + 2 } return } // Bytes returns a []byte representation of event. Strips all newlines and // carriage returns. // // Per RFC2812 section 2.3, messages should not exceed 512 characters in // length. This method forces that limit by discarding any characters // exceeding the length limit. func (e *Event) Bytes() []byte { buffer := new(bytes.Buffer) // Tags. if e.Tags != nil { e.Tags.writeTo(buffer) } // Event prefix. if e.Source != nil { buffer.WriteByte(messagePrefix) e.Source.writeTo(buffer) buffer.WriteByte(eventSpace) } // Command is required. buffer.WriteString(e.Command) // Space separated list of arguments. if len(e.Params) > 0 { buffer.WriteByte(eventSpace) buffer.WriteString(strings.Join(e.Params, string(eventSpace))) } if len(e.Trailing) > 0 || e.EmptyTrailing { buffer.WriteByte(eventSpace) buffer.WriteByte(messagePrefix) buffer.WriteString(e.Trailing) } // We need the limit the buffer length. if buffer.Len() > (maxLength) { buffer.Truncate(maxLength) } out := buffer.Bytes() // Strip newlines and carriage returns. for i := 0; i < len(out); i++ { if out[i] == '\n' || out[i] == '\r' { out = append(out[:i], out[i+1:]...) i-- // Decrease the index so we can pick up where we left off. } } return out } // String returns a string representation of this event. Strips all newlines // and carriage returns. func (e *Event) String() string { return string(e.Bytes()) } // Pretty returns a prettified string of the event. If the event doesn't // support prettification, ok is false. Pretty is not just useful to make // an event prettier, but also to filter out events that most don't visually // see in normal IRC clients. e.g. most clients don't show WHO queries. func (e *Event) Pretty() (out string, ok bool) { if e.Sensitive || e.Echo { return "", false } if e.Command == ERROR { return fmt.Sprintf("[*] an error occurred: %s", e.Trailing), true } if e.Source == nil { if e.Command != PRIVMSG && e.Command != NOTICE { return "", false } if len(e.Params) > 0 && len(e.Trailing) > 0 { return fmt.Sprintf("[>] writing %s [%s]: %s", strings.ToLower(e.Command), strings.Join(e.Params, ", "), e.Trailing), true } else if len(e.Params) > 0 { return fmt.Sprintf("[>] writing %s [%s]", strings.ToLower(e.Command), strings.Join(e.Params, ", ")), true } else if len(e.Trailing) > 0 { return fmt.Sprintf("[>] writing %s: %s", strings.ToLower(e.Command), e.Trailing), true } return "", false } if e.Command == INITIALIZED { return fmt.Sprintf("[*] connection to %s initialized", e.Trailing), true } if e.Command == CONNECTED { return fmt.Sprintf("[*] successfully connected to %s", e.Trailing), true } if (e.Command == PRIVMSG || e.Command == NOTICE) && len(e.Params) > 0 { if ctcp := DecodeCTCP(e); ctcp != nil { if ctcp.Reply { return } if ctcp.Command == CTCP_ACTION { return fmt.Sprintf("[%s] **%s** %s", strings.Join(e.Params, ","), ctcp.Source.Name, ctcp.Text), true } return fmt.Sprintf("[*] CTCP query from %s: %s%s", ctcp.Source.Name, ctcp.Command, " "+ctcp.Text), true } return fmt.Sprintf("[%s] (%s) %s", strings.Join(e.Params, ","), e.Source.Name, e.Trailing), true } if e.Command == RPL_MOTD || e.Command == RPL_MOTDSTART || e.Command == RPL_WELCOME || e.Command == RPL_YOURHOST || e.Command == RPL_CREATED || e.Command == RPL_LUSERCLIENT { return "[*] " + e.Trailing, true } if e.Command == JOIN && len(e.Params) > 0 { return fmt.Sprintf("[*] %s (%s) has joined %s", e.Source.Name, e.Source.Host, e.Params[0]), true } if e.Command == PART && len(e.Params) > 0 { return fmt.Sprintf("[*] %s (%s) has left %s (%s)", e.Source.Name, e.Source.Host, e.Params[0], e.Trailing), true } if e.Command == QUIT { return fmt.Sprintf("[*] %s has quit (%s)", e.Source.Name, e.Trailing), true } if e.Command == INVITE && len(e.Params) == 1 { return fmt.Sprintf("[*] %s invited to %s by %s", e.Params[0], e.Trailing, e.Source.Name), true } if e.Command == KICK && len(e.Params) >= 2 { if e.Trailing == "" && len(e.Params) == 3 { e.Trailing = e.Params[2] } return fmt.Sprintf("[%s] *** %s has kicked %s: %s", e.Params[0], e.Source.Name, e.Params[1], e.Trailing), true } if e.Command == NICK { // Workaround, see https://github.com/lrstanley/girc/pull/15#issuecomment-413845482 var name string if len(e.Params) == 1 { name = e.Params[0] } else if len(e.Trailing) > 0 { name = e.Trailing } if name != "" { return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, name), true } } if e.Command == TOPIC && len(e.Params) > 0 { return fmt.Sprintf("[%s] *** %s has set the topic to: %s", e.Params[len(e.Params)-1], e.Source.Name, e.Trailing), true } if e.Command == MODE && len(e.Params) > 2 { return fmt.Sprintf("[%s] *** %s set modes: %s", e.Params[0], e.Source.Name, strings.Join(e.Params[1:], " ")), true } if e.Command == CAP_AWAY { if len(e.Trailing) > 0 { return fmt.Sprintf("[*] %s is now away: %s", e.Source.Name, e.Trailing), true } return fmt.Sprintf("[*] %s is no longer away", e.Source.Name), true } if e.Command == CAP_CHGHOST && len(e.Params) == 2 { return fmt.Sprintf("[*] %s has changed their host to %s (was %s)", e.Source.Name, e.Params[1], e.Source.Host), true } if e.Command == CAP_ACCOUNT && len(e.Params) == 1 { if e.Params[0] == "*" { return fmt.Sprintf("[*] %s has become un-authenticated", e.Source.Name), true } return fmt.Sprintf("[*] %s has authenticated for account: %s", e.Source.Name, e.Params[0]), true } if e.Command == RPL_TOPIC && len(e.Params) > 0 && len(e.Trailing) > 0 { return fmt.Sprintf("[*] topic for %s is: %s", e.Params[len(e.Params)-1], e.Trailing), true } if e.Command == CAP && len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK { return "[*] enabling capabilities: " + e.Trailing, true } return "", false } // IsAction checks to see if the event is an ACTION (/me). func (e *Event) IsAction() bool { if e.Command != PRIVMSG { return false } ok, ctcp := e.IsCTCP() return ok && ctcp.Command == CTCP_ACTION } // IsCTCP checks to see if the event is a CTCP event, and if so, returns the // converted CTCP event. func (e *Event) IsCTCP() (ok bool, ctcp *CTCPEvent) { ctcp = DecodeCTCP(e) return ctcp != nil, ctcp } // IsFromChannel checks to see if a message was from a channel (rather than // a private message). func (e *Event) IsFromChannel() bool { if e.Source == nil || (e.Command != PRIVMSG && e.Command != NOTICE) || len(e.Params) < 1 { return false } if !IsValidChannel(e.Params[0]) { return false } return true } // IsFromUser checks to see if a message was from a user (rather than a // channel). func (e *Event) IsFromUser() bool { if e.Source == nil || (e.Command != PRIVMSG && e.Command != NOTICE) || len(e.Params) < 1 { return false } if !IsValidNick(e.Params[0]) { return false } return true } // StripAction returns the stripped version of the action encoding from a // PRIVMSG ACTION (/me). func (e *Event) StripAction() string { if !e.IsAction() { return e.Trailing } return e.Trailing[8 : len(e.Trailing)-1] } const ( messagePrefix byte = ':' // Prefix or last argument. prefixIdent byte = '!' // Username. prefixHost byte = '@' // Hostname. ) // Source represents the sender of an IRC event, see RFC1459 section 2.3.1. // <servername> | <nick> [ '!' <user> ] [ '@' <host> ] type Source struct { // Name is the nickname, server name, or service name. Name string `json:"name"` // Ident is commonly known as the "user". Ident string `json:"ident"` // Host is the hostname or IP address of the user/service. Is not accurate // due to how IRC servers can spoof hostnames. Host string `json:"host"` } // Equals compares two Sources for equality. func (s *Source) Equals(ss *Source) bool { if s == nil && ss == nil { return true } if s != nil && ss == nil || s == nil && ss != nil { return false } if s.Name != ss.Name || s.Ident != ss.Ident || s.Host != ss.Host { return false } return true } // Copy returns a deep copy of Source. func (s *Source) Copy() *Source { if s == nil { return nil } newSource := &Source{ Name: s.Name, Ident: s.Ident, Host: s.Host, } return newSource } // ParseSource takes a string and attempts to create a Source struct. func ParseSource(raw string) (src *Source) { src = new(Source) user := strings.IndexByte(raw, prefixIdent) host := strings.IndexByte(raw, prefixHost) switch { case user > 0 && host > user: src.Name = raw[:user] src.Ident = raw[user+1 : host] src.Host = raw[host+1:] case user > 0: src.Name = raw[:user] src.Ident = raw[user+1:] case host > 0: src.Name = raw[:host] src.Host = raw[host+1:] default: src.Name = raw } return src } // Len calculates the length of the string representation of prefix func (s *Source) Len() (length int) { length = len(s.Name) if len(s.Ident) > 0 { length = 1 + length + len(s.Ident) } if len(s.Host) > 0 { length = 1 + length + len(s.Host) } return } // Bytes returns a []byte representation of source. func (s *Source) Bytes() []byte { buffer := new(bytes.Buffer) s.writeTo(buffer) return buffer.Bytes() } // String returns a string representation of source. func (s *Source) String() (out string) { out = s.Name if len(s.Ident) > 0 { out = out + string(prefixIdent) + s.Ident } if len(s.Host) > 0 { out = out + string(prefixHost) + s.Host } return } // IsHostmask returns true if source looks like a user hostmask. func (s *Source) IsHostmask() bool { return len(s.Ident) > 0 && len(s.Host) > 0 } // IsServer returns true if this source looks like a server name. func (s *Source) IsServer() bool { return len(s.Ident) <= 0 && len(s.Host) <= 0 } // writeTo is an utility function to write the source to the bytes.Buffer // in Event.String(). func (s *Source) writeTo(buffer *bytes.Buffer) { buffer.WriteString(s.Name) if len(s.Ident) > 0 { buffer.WriteByte(prefixIdent) buffer.WriteString(s.Ident) } if len(s.Host) > 0 { buffer.WriteByte(prefixHost) buffer.WriteString(s.Host) } return }