package parsers import ( "errors" "fmt" "git.aiterp.net/rpdata/api/models" "strconv" "strings" "time" ) type ParseError struct { Line string Problem string } func (e *ParseError) Error() string { return fmt.Sprintf("Unrecognized post: %s (error: %s)", e.Line, e.Problem) } func IsParseError(err error) bool { _, ok := err.(*ParseError) return ok } // ErrEmptyLog is returned by ParseLog if there are no (valid) posts in the log. var ErrEmptyLog = errors.New("no valid posts found in log") // MircLog parses the log and returns the things that can be gleamed from them. func MircLog(data string, date time.Time, strict bool) (*ParsedLog, error) { lines := strings.Split(data, "\n") posts := make([]*models.Post, 0, len(lines)) prev := models.Post{} for _, line := range lines { line = strings.Trim(line, "\r\t  ") if len(line) < 1 { continue } post, err := MircPost(line, date, prev) if err != nil { if strict { return nil, err } continue } posts = append(posts, &post) prev = post } if len(posts) == 0 { return nil, ErrEmptyLog } log := models.Log{ Date: posts[0].Time, } return &ParsedLog{log, posts}, nil } // MircPost parses a post from a mirc-like line. If the previous post is included (it can be empty), it will be used // to determine whether midnight has passed. func MircPost(line string, date time.Time, prev models.Post) (models.Post, error) { // Do basic validation line = strings.Trim(line, "  \t\n\r") if len(line) == 0 || !strings.HasPrefix(line, "[") { return models.Post{}, &ParseError{ Line: line, Problem: "no timestamp", } } // Parse timestamp tsEndIndex := strings.IndexByte(line, ']') if tsEndIndex == -1 || len(line) < tsEndIndex+5 { return models.Post{}, &ParseError{ Line: line, Problem: "incomplete timestamp", } } tsStr := line[1:tsEndIndex] tsSplit := strings.Split(tsStr, ":") tsUnits := make([]int, len(tsSplit)) if len(tsSplit) < 2 { return models.Post{}, &ParseError{ Line: line, Problem: "invalid timestamp", } } for i := range tsSplit { n, err := strconv.Atoi(tsSplit[i]) if err != nil { return models.Post{}, &ParseError{ Line: line, Problem: "invalid number in timestamp", } } tsUnits[i] = n } if len(tsUnits) == 2 { tsUnits = append(tsUnits, 0) } // Determine timestamp from parsed data and previous post. if !prev.Time.IsZero() { date = prev.Time } ts := time.Date(date.Year(), date.Month(), date.Day(), tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location()) if ts.Before(prev.Time) { ts = ts.Add(time.Hour * 24) } if line[tsEndIndex+2] == '*' { split := strings.SplitN(line[tsEndIndex+4:], " ", 2) if len(split) == 1 { return models.Post{}, &ParseError{ Line: line, Problem: "post is empty", } } post := models.Post{ ID: "UNASSIGNED", LogID: "UNASSIGNED", Time: ts, Kind: "action", Nick: strings.Trim(strings.TrimLeft(split[0], "+@!~\u001F"), "\u001F"), Text: split[1], Position: prev.Position + 1, } if post.Nick[0] == '=' { post.Kind = "scene" } return post, nil } else if line[tsEndIndex+2] == '<' { split := strings.SplitN(line[tsEndIndex+2:], " ", 2) if len(split) == 1 { return models.Post{}, &ParseError{ Line: line, Problem: "post is empty", } } post := models.Post{ ID: "UNASSIGNED", LogID: "UNASSIGNED", Time: ts, Kind: "text", Nick: strings.Trim(strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"), "\u001F"), Text: split[1], Position: prev.Position + 1, } if post.Nick[0] == '=' { post.Kind = "scene" } return post, nil } else { return models.Post{}, &ParseError{ Line: line, Problem: "line is neither action nor text post", } } }