|
|
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", } } }
|