summaryrefslogtreecommitdiff
path: root/vendor/github.com/fluffle/goirc/client/line.go
blob: bfa473a821848badf2944921cb3d93110e30b249 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package client

import (
	"runtime"
	"strings"
	"time"

	"github.com/fluffle/goirc/logging"
)

var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")

// We parse an incoming line into this struct. Line.Cmd is used as the trigger
// name for incoming event handlers and is the IRC verb, the first sequence
// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG.
//   Raw =~ ":nick!user@host cmd args[] :text"
//   Src == "nick!user@host"
//   Cmd == e.g. PRIVMSG, 332
type Line struct {
	Tags                   map[string]string
	Nick, Ident, Host, Src string
	Cmd, Raw               string
	Args                   []string
	Time                   time.Time
}

// Copy returns a deep copy of the Line.
func (l *Line) Copy() *Line {
	nl := *l
	nl.Args = make([]string, len(l.Args))
	copy(nl.Args, l.Args)
	if l.Tags != nil {
		nl.Tags = make(map[string]string)
		for k, v := range l.Tags {
			nl.Tags[k] = v
		}
	}
	return &nl
}

// Text returns the contents of the text portion of a line. This only really
// makes sense for lines with a :text part, but there are a lot of them.
func (line *Line) Text() string {
	if len(line.Args) > 0 {
		return line.Args[len(line.Args)-1]
	}
	return ""
}

// Target returns the contextual target of the line, usually the first Arg
// for the IRC verb. If the line was broadcast from a channel, the target
// will be that channel. If the line was sent directly by a user, the target
// will be that user.
func (line *Line) Target() string {
	// TODO(fluffle): Add 005 CHANTYPES parsing for this?
	switch line.Cmd {
	case PRIVMSG, NOTICE, ACTION:
		if !line.Public() {
			return line.Nick
		}
	case CTCP, CTCPREPLY:
		if !line.Public() {
			return line.Nick
		}
		return line.Args[1]
	}
	if len(line.Args) > 0 {
		return line.Args[0]
	}
	return ""
}

// Public returns true if the line is the result of an IRC user sending
// a message to a channel the client has joined instead of directly
// to the client.
//
// NOTE: This is very permissive, allowing all 4 RFC channel types even if
// your server doesn't technically support them.
func (line *Line) Public() bool {
	switch line.Cmd {
	case PRIVMSG, NOTICE, ACTION:
		switch line.Args[0][0] {
		case '#', '&', '+', '!':
			return true
		}
	case CTCP, CTCPREPLY:
		// CTCP prepends the CTCP verb to line.Args, thus for the message
		//   :nick!user@host PRIVMSG #foo :\001BAR baz\001
		// line.Args contains: []string{"BAR", "#foo", "baz"}
		// TODO(fluffle): Arguably this is broken, and we should have
		// line.Args containing: []string{"#foo", "BAR", "baz"}
		// ... OR change conn.Ctcp()'s argument order to be consistent.
		switch line.Args[1][0] {
		case '#', '&', '+', '!':
			return true
		}
	}
	return false
}

// ParseLine creates a Line from an incoming message from the IRC server.
//
// It contains special casing for CTCP messages, most notably CTCP ACTION.
// All CTCP messages have the \001 bytes stripped from the message and the
// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.
//
// ParseLine also parses IRCv3 tags, if received. If a line does not have
// the tags section, Line.Tags will be nil. Tags are optional, and will
// only be included after the correct CAP command.
//
// http://ircv3.net/specs/core/capability-negotiation-3.1.html
// http://ircv3.net/specs/core/message-tags-3.2.html
func ParseLine(s string) *Line {
	line := &Line{Raw: s}

	if s == "" {
		return nil
	}

	if s[0] == '@' {
		var rawTags string
		line.Tags = make(map[string]string)
		if idx := strings.Index(s, " "); idx != -1 {
			rawTags, s = s[1:idx], s[idx+1:]
		} else {
			return nil
		}

		// ; is represented as \: in a tag, so it's safe to split on ;
		for _, tag := range strings.Split(rawTags, ";") {
			if tag == "" {
				continue
			}

			pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2)
			if len(pair) < 2 {
				line.Tags[tag] = ""
			} else {
				line.Tags[pair[0]] = pair[1]
			}
		}
	}

	if s[0] == ':' {
		// remove a source and parse it
		if idx := strings.Index(s, " "); idx != -1 {
			line.Src, s = s[1:idx], s[idx+1:]
		} else {
			// pretty sure we shouldn't get here ...
			return nil
		}

		// src can be the hostname of the irc server or a nick!user@host
		line.Host = line.Src
		nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@")
		if uidx != -1 && nidx != -1 {
			line.Nick = line.Src[:nidx]
			line.Ident = line.Src[nidx+1 : uidx]
			line.Host = line.Src[uidx+1:]
		}
	}

	// now we're here, we've parsed a :nick!user@host or :server off
	// s should contain "cmd args[] :text"
	args := strings.SplitN(s, " :", 2)
	if len(args) > 1 {
		args = append(strings.Fields(args[0]), args[1])
	} else {
		args = strings.Fields(args[0])
	}
	line.Cmd = strings.ToUpper(args[0])
	if len(args) > 1 {
		line.Args = args[1:]
	}

	// So, I think CTCP and (in particular) CTCP ACTION are better handled as
	// separate events as opposed to forcing people to have gargantuan
	// handlers to cope with the possibilities.
	if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) &&
		len(line.Args[1]) > 2 &&
		strings.HasPrefix(line.Args[1], "\001") &&
		strings.HasSuffix(line.Args[1], "\001") {
		// WOO, it's a CTCP message
		t := strings.SplitN(strings.Trim(line.Args[1], "\001"), " ", 2)
		if len(t) > 1 {
			// Replace the line with the unwrapped CTCP
			line.Args[1] = t[1]
		}
		if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG {
			// make a CTCP ACTION it's own event a-la PRIVMSG
			line.Cmd = c
		} else {
			// otherwise, dispatch a generic CTCP/CTCPREPLY event that
			// contains the type of CTCP in line.Args[0]
			if line.Cmd == PRIVMSG {
				line.Cmd = CTCP
			} else {
				line.Cmd = CTCPREPLY
			}
			line.Args = append([]string{c}, line.Args...)
		}
	}
	return line
}

func (line *Line) argslen(minlen int) bool {
	pc, _, _, _ := runtime.Caller(1)
	fn := runtime.FuncForPC(pc)
	if len(line.Args) <= minlen {
		logging.Warn("%s: too few arguments: %s", fn.Name(), strings.Join(line.Args, " "))
		return false
	}
	return true
}