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 217 218 219 220 221 222 223 224
|
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
if n, i, h, ok := parseUserHost(line.Src); ok {
line.Nick = n
line.Ident = i
line.Host = h
}
}
// 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 parseUserHost(uh string) (nick, ident, host string, ok bool) {
uh = strings.TrimSpace(uh)
nidx, uidx := strings.Index(uh, "!"), strings.Index(uh, "@")
if uidx == -1 || nidx == -1 {
return "", "", "", false
}
return uh[:nidx], uh[nidx+1 : uidx], uh[uidx+1:], true
}
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
}
|