1package client
2
3import (
4	"runtime"
5	"strings"
6	"time"
7
8	"github.com/fluffle/goirc/logging"
9)
10
11var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
12
13// We parse an incoming line into this struct. Line.Cmd is used as the trigger
14// name for incoming event handlers and is the IRC verb, the first sequence
15// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG.
16//   Raw =~ ":nick!user@host cmd args[] :text"
17//   Src == "nick!user@host"
18//   Cmd == e.g. PRIVMSG, 332
19type Line struct {
20	Tags                   map[string]string
21	Nick, Ident, Host, Src string
22	Cmd, Raw               string
23	Args                   []string
24	Time                   time.Time
25}
26
27// Copy returns a deep copy of the Line.
28func (l *Line) Copy() *Line {
29	nl := *l
30	nl.Args = make([]string, len(l.Args))
31	copy(nl.Args, l.Args)
32	if l.Tags != nil {
33		nl.Tags = make(map[string]string)
34		for k, v := range l.Tags {
35			nl.Tags[k] = v
36		}
37	}
38	return &nl
39}
40
41// Text returns the contents of the text portion of a line. This only really
42// makes sense for lines with a :text part, but there are a lot of them.
43func (line *Line) Text() string {
44	if len(line.Args) > 0 {
45		return line.Args[len(line.Args)-1]
46	}
47	return ""
48}
49
50// Target returns the contextual target of the line, usually the first Arg
51// for the IRC verb. If the line was broadcast from a channel, the target
52// will be that channel. If the line was sent directly by a user, the target
53// will be that user.
54func (line *Line) Target() string {
55	// TODO(fluffle): Add 005 CHANTYPES parsing for this?
56	switch line.Cmd {
57	case PRIVMSG, NOTICE, ACTION:
58		if !line.Public() {
59			return line.Nick
60		}
61	case CTCP, CTCPREPLY:
62		if !line.Public() {
63			return line.Nick
64		}
65		return line.Args[1]
66	}
67	if len(line.Args) > 0 {
68		return line.Args[0]
69	}
70	return ""
71}
72
73// Public returns true if the line is the result of an IRC user sending
74// a message to a channel the client has joined instead of directly
75// to the client.
76//
77// NOTE: This is very permissive, allowing all 4 RFC channel types even if
78// your server doesn't technically support them.
79func (line *Line) Public() bool {
80	switch line.Cmd {
81	case PRIVMSG, NOTICE, ACTION:
82		switch line.Args[0][0] {
83		case '#', '&', '+', '!':
84			return true
85		}
86	case CTCP, CTCPREPLY:
87		// CTCP prepends the CTCP verb to line.Args, thus for the message
88		//   :nick!user@host PRIVMSG #foo :\001BAR baz\001
89		// line.Args contains: []string{"BAR", "#foo", "baz"}
90		// TODO(fluffle): Arguably this is broken, and we should have
91		// line.Args containing: []string{"#foo", "BAR", "baz"}
92		// ... OR change conn.Ctcp()'s argument order to be consistent.
93		switch line.Args[1][0] {
94		case '#', '&', '+', '!':
95			return true
96		}
97	}
98	return false
99}
100
101// ParseLine creates a Line from an incoming message from the IRC server.
102//
103// It contains special casing for CTCP messages, most notably CTCP ACTION.
104// All CTCP messages have the \001 bytes stripped from the message and the
105// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
106// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
107// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.
108//
109// ParseLine also parses IRCv3 tags, if received. If a line does not have
110// the tags section, Line.Tags will be nil. Tags are optional, and will
111// only be included after the correct CAP command.
112//
113// http://ircv3.net/specs/core/capability-negotiation-3.1.html
114// http://ircv3.net/specs/core/message-tags-3.2.html
115func ParseLine(s string) *Line {
116	line := &Line{Raw: s}
117
118	if s == "" {
119		return nil
120	}
121
122	if s[0] == '@' {
123		var rawTags string
124		line.Tags = make(map[string]string)
125		if idx := strings.Index(s, " "); idx != -1 {
126			rawTags, s = s[1:idx], s[idx+1:]
127		} else {
128			return nil
129		}
130
131		// ; is represented as \: in a tag, so it's safe to split on ;
132		for _, tag := range strings.Split(rawTags, ";") {
133			if tag == "" {
134				continue
135			}
136
137			pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2)
138			if len(pair) < 2 {
139				line.Tags[tag] = ""
140			} else {
141				line.Tags[pair[0]] = pair[1]
142			}
143		}
144	}
145
146	if s[0] == ':' {
147		// remove a source and parse it
148		if idx := strings.Index(s, " "); idx != -1 {
149			line.Src, s = s[1:idx], s[idx+1:]
150		} else {
151			// pretty sure we shouldn't get here ...
152			return nil
153		}
154
155		// src can be the hostname of the irc server or a nick!user@host
156		line.Host = line.Src
157		nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@")
158		if uidx != -1 && nidx != -1 {
159			line.Nick = line.Src[:nidx]
160			line.Ident = line.Src[nidx+1 : uidx]
161			line.Host = line.Src[uidx+1:]
162		}
163	}
164
165	// now we're here, we've parsed a :nick!user@host or :server off
166	// s should contain "cmd args[] :text"
167	args := strings.SplitN(s, " :", 2)
168	if len(args) > 1 {
169		args = append(strings.Fields(args[0]), args[1])
170	} else {
171		args = strings.Fields(args[0])
172	}
173	line.Cmd = strings.ToUpper(args[0])
174	if len(args) > 1 {
175		line.Args = args[1:]
176	}
177
178	// So, I think CTCP and (in particular) CTCP ACTION are better handled as
179	// separate events as opposed to forcing people to have gargantuan
180	// handlers to cope with the possibilities.
181	if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) &&
182		len(line.Args[1]) > 2 &&
183		strings.HasPrefix(line.Args[1], "\001") &&
184		strings.HasSuffix(line.Args[1], "\001") {
185		// WOO, it's a CTCP message
186		t := strings.SplitN(strings.Trim(line.Args[1], "\001"), " ", 2)
187		if len(t) > 1 {
188			// Replace the line with the unwrapped CTCP
189			line.Args[1] = t[1]
190		}
191		if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG {
192			// make a CTCP ACTION it's own event a-la PRIVMSG
193			line.Cmd = c
194		} else {
195			// otherwise, dispatch a generic CTCP/CTCPREPLY event that
196			// contains the type of CTCP in line.Args[0]
197			if line.Cmd == PRIVMSG {
198				line.Cmd = CTCP
199			} else {
200				line.Cmd = CTCPREPLY
201			}
202			line.Args = append([]string{c}, line.Args...)
203		}
204	}
205	return line
206}
207
208func (line *Line) argslen(minlen int) bool {
209	pc, _, _, _ := runtime.Caller(1)
210	fn := runtime.FuncForPC(pc)
211	if len(line.Args) <= minlen {
212		logging.Warn("%s: too few arguments: %s", fn.Name(), strings.Join(line.Args, " "))
213		return false
214	}
215	return true
216}
217