GraphQL API and utilities for the rpdata project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

172 lines
3.8 KiB

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