summaryrefslogtreecommitdiff
path: root/teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go
diff options
context:
space:
mode:
authorMistivia <i@mistivia.com>2025-11-02 15:27:18 +0800
committerMistivia <i@mistivia.com>2025-11-02 15:27:18 +0800
commite9c24f4af7ed56760f6db7941827d09f6db9020b (patch)
tree62128c43b883ce5e3148113350978755779bb5de /teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go
parent58d5e7cfda4781d8a57ec52aefd02983835c301a (diff)
add matterbridge
Diffstat (limited to 'teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go')
-rw-r--r--teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go640
1 files changed, 640 insertions, 0 deletions
diff --git a/teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go
new file mode 100644
index 0000000..7801615
--- /dev/null
+++ b/teleirc/matterbridge/vendor/github.com/lrstanley/girc/event.go
@@ -0,0 +1,640 @@
+// 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 int = 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'
+}
+
+// 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:]
+ i = 0
+ }
+
+ if raw != "" && 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.Fields(raw[j:])
+ 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.Fields(raw[j : i-1])
+ }
+
+ e.Params = append(e.Params, raw[i+1:])
+
+ return e
+}
+
+// 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.
+ // The last item in the slice could potentially contain spaces (commonly
+ // referred to as the "trailing" parameter).
+ Params []string `json:"params"`
+ // 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"`
+}
+
+// Last returns the last parameter in Event.Params if it exists.
+func (e *Event) Last() string {
+ if len(e.Params) >= 1 {
+ return e.Params[len(e.Params)-1]
+ }
+ return ""
+}
+
+// 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,
+ 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 || 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 {
+ // Spaces before each param.
+ length += len(e.Params)
+
+ for i := 0; i < len(e.Params); i++ {
+ length += len(e.Params[i])
+
+ // If param contains a space or it's empty, it's trailing, so it should be
+ // prefixed with a colon (:).
+ if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") {
+ length++
+ }
+ }
+ }
+
+ 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 {
+ for i := 0; i < len(e.Params); i++ {
+ if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") {
+ buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i])
+ continue
+ }
+ buffer.WriteString(string(eventSpace) + e.Params[i])
+ }
+ }
+
+ // We need the limit the buffer length.
+ if buffer.Len() > (maxLength) {
+ buffer.Truncate(maxLength)
+ }
+
+ // If we truncated in the middle of a utf8 character, we need to remove
+ // the other (now invalid) bytes.
+ out := bytes.ToValidUTF8(buffer.Bytes(), nil)
+
+ // 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.Last()), true
+ }
+
+ if e.Source == nil {
+ if e.Command != PRIVMSG && e.Command != NOTICE {
+ return "", false
+ }
+
+ if len(e.Params) > 0 {
+ return fmt.Sprintf("[>] writing %s", e.String()), true
+ }
+
+ return "", false
+ }
+
+ if e.Command == INITIALIZED {
+ return fmt.Sprintf("[*] connection to %s initialized", e.Last()), true
+ }
+
+ if e.Command == CONNECTED {
+ return fmt.Sprintf("[*] successfully connected to %s", e.Last()), 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[0:len(e.Params)-1], ","), ctcp.Source.Name, ctcp.Text), true
+ }
+
+ return fmt.Sprintf("[*] CTCP query from %s: %s%s", ctcp.Source.Name, ctcp.Command, " "+ctcp.Text), true
+ }
+
+ var source string
+ if e.Command == PRIVMSG {
+ source = fmt.Sprintf("(%s)", e.Source.Name)
+ } else { // NOTICE
+ source = fmt.Sprintf("--%s--", e.Source.Name)
+ }
+
+ return fmt.Sprintf("[%s] %s %s", strings.Join(e.Params[0:len(e.Params)-1], ","), source, e.Last()), 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.Last(), 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.Last()), true
+ }
+
+ if e.Command == QUIT {
+ return fmt.Sprintf("[*] %s has quit (%s)", e.Source.Name, e.Last()), true
+ }
+
+ if e.Command == INVITE && len(e.Params) == 1 {
+ return fmt.Sprintf("[*] %s invited to %s by %s", e.Params[0], e.Last(), e.Source.Name), 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.Last()), true
+ }
+
+ if e.Command == NICK {
+ return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, e.Last()), true
+ }
+
+ if e.Command == TOPIC && len(e.Params) >= 2 {
+ return fmt.Sprintf("[%s] *** %s has set the topic to: %s", e.Params[0], e.Source.Name, e.Last()), true
+ }
+
+ if e.Command == RPL_TOPIC && len(e.Params) > 0 {
+ if len(e.Params) >= 2 {
+ return fmt.Sprintf("[*] topic for %s is: %s", e.Params[1], e.Last()), true
+ }
+ return fmt.Sprintf("[*] topic for %s is: %s", e.Params[0], e.Last()), 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.Params) > 0 {
+ return fmt.Sprintf("[*] %s is now away: %s", e.Source.Name, e.Last()), 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 == CAP && len(e.Params) >= 2 && e.Params[1] == CAP_ACK {
+ return "[*] enabling capabilities: " + e.Last(), 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.Last()
+ }
+
+ msg := e.Last()
+ return msg[8 : len(msg)-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, in its original
+ // non-rfc1459 form.
+ 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"`
+}
+
+// ID is the nickname, server name, or service name, in it's converted
+// and comparable) form.
+func (s *Source) ID() string {
+ return ToRFC1459(s.Name)
+}
+
+// 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.ID() != ss.ID() || 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 s.Ident == "" && s.Host == ""
+}
+
+// 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)
+ }
+}