diff options
Diffstat (limited to 'irc/tokens.go')
-rw-r--r-- | irc/tokens.go | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/irc/tokens.go b/irc/tokens.go new file mode 100644 index 0000000..976207d --- /dev/null +++ b/irc/tokens.go @@ -0,0 +1,327 @@ +package irc + +import ( + "errors" + "fmt" + "strings" + "time" +) + +func word(s string) (w, rest string) { + split := strings.SplitN(s, " ", 2) + + if len(split) < 2 { + w = split[0] + rest = "" + } else { + w = split[0] + rest = split[1] + } + + return +} + +func tagEscape(c rune) (escape rune) { + switch c { + case ':': + escape = ';' + case 's': + escape = ' ' + case 'r': + escape = '\r' + case 'n': + escape = '\n' + default: + escape = c + } + + return +} + +func unescapeTagValue(escaped string) (unescaped string) { + var builder strings.Builder + builder.Grow(len(escaped)) + escape := false + + for _, c := range escaped { + if c == '\\' && !escape { + escape = true + } else { + var cpp rune + + if escape { + cpp = tagEscape(c) + } else { + cpp = c + } + + builder.WriteRune(cpp) + escape = false + } + } + + unescaped = builder.String() + return +} + +func parseTags(s string) (tags map[string]string) { + s = s[1:] + tags = map[string]string{} + + for _, item := range strings.Split(s, ";") { + if item == "" || item == "=" || item == "+" || item == "+=" { + continue + } + + kv := strings.SplitN(item, "=", 2) + if len(kv) < 2 { + tags[kv[0]] = "" + } else { + tags[kv[0]] = unescapeTagValue(kv[1]) + } + } + + return +} + +var ( + errEmptyMessage = errors.New("empty message") + errIncompleteMessage = errors.New("message is incomplete") +) + +var ( + errNoPrefix = errors.New("missing prefix") + errNotEnoughParams = errors.New("not enough params") + errUnknownCommand = errors.New("unknown command") +) + +type Message struct { + Tags map[string]string + Prefix string + Command string + Params []string +} + +func Tokenize(line string) (msg Message, err error) { + line = strings.TrimLeft(line, " ") + if line == "" { + err = errEmptyMessage + return + } + + if line[0] == '@' { + var tags string + + tags, line = word(line) + msg.Tags = parseTags(tags) + } + + line = strings.TrimLeft(line, " ") + if line == "" { + err = errIncompleteMessage + return + } + + if line[0] == ':' { + var prefix string + + prefix, line = word(line) + msg.Prefix = prefix[1:] + } + + line = strings.TrimLeft(line, " ") + if line == "" { + err = errIncompleteMessage + return + } + + msg.Command, line = word(line) + msg.Command = strings.ToUpper(msg.Command) + + msg.Params = make([]string, 0, 15) + for line != "" { + if line[0] == ':' { + msg.Params = append(msg.Params, line[1:]) + break + } + + var param string + param, line = word(line) + msg.Params = append(msg.Params, param) + } + + return +} + +func (msg *Message) Validate() (err error) { + switch msg.Command { + case "001": + if len(msg.Params) < 1 { + err = errNotEnoughParams + } + case "005": + if len(msg.Params) < 3 { + err = errNotEnoughParams + } + case "352": + if len(msg.Params) < 8 { + err = errNotEnoughParams + } + case "372": + if len(msg.Params) < 2 { + err = errNotEnoughParams + } + case "AUTHENTICATE": + if len(msg.Params) < 1 { + err = errNotEnoughParams + } + case "900": + if len(msg.Params) < 3 { + err = errNotEnoughParams + } + case "901": + if len(msg.Params) < 2 { + err = errNotEnoughParams + } + case "CAP": + if len(msg.Params) < 3 { + err = errNotEnoughParams + } else if msg.Params[1] == "LS" { + } else if msg.Params[1] == "LIST" { + } else if msg.Params[1] == "ACK" { + } else if msg.Params[1] == "NAK" { + } else if msg.Params[1] == "NEW" { + } else if msg.Params[1] == "DEL" { + } else { + err = errUnknownCommand + } + case "JOIN": + if len(msg.Params) < 1 { + err = errNotEnoughParams + } else if msg.Prefix == "" { + err = errNoPrefix + } + case "353": + if len(msg.Params) < 4 { + err = errNotEnoughParams + } + case "332": + if len(msg.Params) < 3 { + err = errNotEnoughParams + } + case "TOPIC": + if len(msg.Params) < 2 { + err = errNotEnoughParams + } + case "PING": + if len(msg.Params) < 1 { + err = errNotEnoughParams + } + case "PONG": + if len(msg.Params) < 1 { + err = errNotEnoughParams + } + default: + } + return +} + +func (msg *Message) Time() (t time.Time, ok bool) { + var tag string + var year, month, day, hour, minute, second, millis int + + tag, ok = msg.Tags["time"] + if !ok { + return + } + + tag = strings.TrimSuffix(tag, "Z") + + _, err := fmt.Sscanf(tag, "%4d-%2d-%2dT%2d:%2d:%2d.%3d", &year, &month, &day, &hour, &minute, &second, &millis) + if err != nil || month < 1 || 12 < month { + ok = false + return + } + + t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1000000, time.Local) + + return +} + +func FullMask(s string) (nick, user, host string) { + if s == "" { + return + } + + spl0 := strings.Split(s, "@") + if 1 < len(spl0) { + host = spl0[1] + } + + spl1 := strings.Split(spl0[0], "!") + if 1 < len(spl1) { + user = spl1[1] + } + + nick = spl1[0] + + return +} + +type Cap struct { + Name string + Value string + Enable bool +} + +func TokenizeCaps(caps string) (diff []Cap) { + for _, c := range strings.Split(caps, " ") { + if c == "" || c == "-" || c == "=" || c == "-=" { + continue + } + + var item Cap + + if strings.HasPrefix(c, "-") { + item.Enable = false + c = c[1:] + } else { + item.Enable = true + } + + kv := strings.SplitN(c, "=", 2) + item.Name = strings.ToLower(kv[0]) + if len(kv) > 1 { + item.Value = kv[1] + } + + diff = append(diff, item) + } + + return +} + +type Name struct { + PowerLevel string + Nick string + User string + Host string +} + +func TokenizeNames(trailing string, prefixes string) (names []Name) { + for _, name := range strings.Split(trailing, " ") { + if name == "" { + continue + } + + var item Name + + mask := strings.TrimLeft(name, prefixes) + item.Nick, item.User, item.Host = FullMask(mask) + item.PowerLevel = name[:len(name)-len(mask)] + + names = append(names, item) + } + + return +} |