162 lines
3.4 KiB
Go
162 lines
3.4 KiB
Go
package parsers
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"git.aiterp.net/rpdata/api/models"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
var ErrSkip = errors.New("parsers: skip this post")
|
||
|
||
func IRCCloudLogs(data string, location *time.Location, threshold time.Duration) ([]ParsedLog, error) {
|
||
lines := strings.Split(data, "\n")
|
||
pos := 0
|
||
results := make([]ParsedLog, 0, 8)
|
||
for pos < len(lines) {
|
||
log, n, err := IRCCloudLog(lines[pos:], location, threshold)
|
||
if err != nil {
|
||
if err == ErrEmptyLog {
|
||
pos += n
|
||
continue
|
||
}
|
||
|
||
return nil, err
|
||
}
|
||
|
||
pos += n
|
||
results = append(results, *log)
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// IRCCloudLog parses the log and returns the things that can be gleamed from them.
|
||
func IRCCloudLog(lines []string, location *time.Location, threshold time.Duration) (*ParsedLog, int, error) {
|
||
posts := make([]*models.Post, 0, len(lines))
|
||
prev := (*models.Post)(nil)
|
||
amount := 0
|
||
|
||
for _, line := range lines {
|
||
line = strings.Trim(line, "\r\t ")
|
||
if len(line) < 1 {
|
||
amount += 1
|
||
continue
|
||
}
|
||
|
||
post, err := IRCCloudPost(line, location)
|
||
if err == ErrSkip {
|
||
amount += 1
|
||
continue
|
||
} else if err != nil {
|
||
return nil, -1, err
|
||
}
|
||
|
||
if prev != nil {
|
||
if post.Time.Sub(prev.Time) >= threshold {
|
||
break
|
||
}
|
||
|
||
post.Position = prev.Position + 1
|
||
} else {
|
||
post.Position = 1
|
||
}
|
||
|
||
posts = append(posts, &post)
|
||
prev = &post
|
||
amount += 1
|
||
}
|
||
|
||
if len(posts) == 0 {
|
||
return nil, amount, ErrEmptyLog
|
||
}
|
||
|
||
log := models.Log{
|
||
Date: posts[0].Time,
|
||
}
|
||
|
||
return &ParsedLog{log, posts}, amount, nil
|
||
}
|
||
|
||
// IRCCloudPost 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 IRCCloudPost(line string, tz *time.Location) (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]
|
||
|
||
ts, err := time.ParseInLocation("2006-01-02 15:04:05", tsStr, tz)
|
||
if err != nil {
|
||
return models.Post{}, &ParseError{
|
||
Line: line,
|
||
Problem: fmt.Sprintf("Could not parse date: %s", err.Error()),
|
||
}
|
||
}
|
||
|
||
if strings.HasPrefix(line[tsEndIndex+2:], "—") {
|
||
split := strings.SplitN(line[tsEndIndex+6:], " ", 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"),
|
||
Text: split[1],
|
||
}
|
||
|
||
if strings.HasPrefix(post.Nick, "=") {
|
||
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],
|
||
}
|
||
|
||
if strings.HasPrefix(post.Nick, "=") {
|
||
post.Kind = "scene"
|
||
}
|
||
|
||
return post, nil
|
||
} else {
|
||
return models.Post{}, ErrSkip
|
||
}
|
||
}
|