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