summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lrstanley/girc/event.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/lrstanley/girc/event.go')
-rw-r--r--vendor/github.com/lrstanley/girc/event.go550
1 files changed, 550 insertions, 0 deletions
diff --git a/vendor/github.com/lrstanley/girc/event.go b/vendor/github.com/lrstanley/girc/event.go
new file mode 100644
index 00000000..77d9b45e
--- /dev/null
+++ b/vendor/github.com/lrstanley/girc/event.go
@@ -0,0 +1,550 @@
+// 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"
+)
+
+const (
+ eventSpace byte = 0x20 // 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 *Source `json:"source"` // The source of the event.
+ Tags Tags `json:"tags"` // IRCv3 style message tags. Only use if network supported.
+ Command string `json:"command"` // the IRC command, e.g. JOIN, PRIVMSG, KILL.
+ Params []string `json:"params"` // parameters to the command. Commonly nickname, channel, etc.
+ Trailing string `json:"trailing"` // any trailing data. e.g. with a PRIVMSG, this is the message text.
+ EmptyTrailing bool `json:"empty_trailing"` // if true, trailing prefix (:) will be added even if Event.Trailing is empty.
+ Sensitive bool `json:"sensitive"` // if the message is sensitive (e.g. and should not be logged).
+}
+
+// ParseEvent takes a string and attempts to create a Event struct.
+//
+// Returns nil if the Event is invalid.
+func ParseEvent(raw string) (e *Event) {
+ // Ignore empty events.
+ if raw = strings.TrimFunc(raw, cutCRFunc); len(raw) < 2 {
+ return nil
+ }
+
+ var i, j int
+ e = &Event{}
+
+ 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])
+ 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{
+ Command: e.Command,
+ Trailing: e.Trailing,
+ EmptyTrailing: e.EmptyTrailing,
+ Sensitive: e.Sensitive,
+ }
+
+ // 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
+}
+
+// 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] == 0x0A || out[i] == 0x0D {
+ 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 {
+ 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
+ }
+
+ 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 == KICK && len(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 && len(e.Params) == 1 {
+ return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, e.Params[0]), 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
+ }
+
+ return "", false
+}
+
+// IsAction checks to see if the event is a PRIVMSG, and is an ACTION (/me).
+func (e *Event) IsAction() bool {
+ if e.Source == nil || e.Command != PRIVMSG || len(e.Trailing) < 9 {
+ return false
+ }
+
+ if !strings.HasPrefix(e.Trailing, "\001ACTION") || e.Trailing[len(e.Trailing)-1] != ctcpDelim {
+ return false
+ }
+
+ return true
+}
+
+// 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 || 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 || 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 = 0x3A // ":" -- prefix or last argument
+ prefixIdent byte = 0x21 // "!" -- username
+ prefixHost byte = 0x40 // "@" -- 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"`
+}
+
+// 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
+}