Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4eabd69ac | ||
|
|
df982fac17 | ||
|
|
6fa2dc9800 | ||
|
|
6ce38d61b1 | ||
|
|
dc3f77f8da | ||
|
|
6321ea1893 | ||
|
|
9b346fb97b | ||
|
|
ed21b7c8cd |
+34
-16
@@ -1,21 +1,39 @@
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/git.aiterp.net/rpdata/api
|
||||
name: rpdata-api
|
||||
|
||||
pipeline:
|
||||
docker-release:
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: go-test
|
||||
image: golang:1.14
|
||||
commands:
|
||||
- go generate ./...
|
||||
- go test -v ./...
|
||||
|
||||
- name: docker-release-tag
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata
|
||||
auto_tag: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
settings:
|
||||
auto_tag: true
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: r.vmaple.dev/rpdata/rpdata-api
|
||||
registry: r.vmaple.dev
|
||||
when:
|
||||
event: tag
|
||||
ref: refs/tags/*
|
||||
docker-release-latest:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: docker-release-latest
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata
|
||||
tags: latest
|
||||
secrets: [ docker_username, docker_password ]
|
||||
settings:
|
||||
auto_tag: false
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: r.vmaple.dev/rpdata/rpdata-api
|
||||
registry: r.vmaple.dev
|
||||
when:
|
||||
event: tag
|
||||
ref: refs/tags/*
|
||||
event:
|
||||
- tag
|
||||
@@ -10,6 +10,9 @@ rpdata-*
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
/main
|
||||
/main.exe
|
||||
|
||||
/graph2/graphcore/*_gen.go
|
||||
generated.gql
|
||||
generated.go
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
## 1. Build Server
|
||||
# Use Go 1.12
|
||||
FROM golang:1.12 AS build-server
|
||||
# Use Go 1.14
|
||||
FROM golang:1.14 AS build-server
|
||||
|
||||
# Load repository into docker
|
||||
WORKDIR /project/
|
||||
@@ -15,7 +15,7 @@ RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-server ./cm
|
||||
|
||||
## 2. Distribute
|
||||
# Use alpine linux
|
||||
FROM alpine:3.7
|
||||
FROM alpine:3.12
|
||||
|
||||
# Install certificates to enable HTTPS GET requests, and tzdata for log imports.
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
@@ -55,7 +55,12 @@ func (r *mutationResolver) ImportLog(ctx context.Context, input graphcore.LogImp
|
||||
tz = parsedTZ
|
||||
}
|
||||
|
||||
return r.s.Logs.Import(ctx, input.Importer, date, tz, input.ChannelName, input.Data)
|
||||
sessionThreshold := time.Hour * 12
|
||||
if input.SessionThresholdMs != nil {
|
||||
sessionThreshold = time.Duration(*input.SessionThresholdMs) * time.Millisecond
|
||||
}
|
||||
|
||||
return r.s.Logs.Import(ctx, input.Importer, date, tz, input.ChannelName, sessionThreshold, input.Data)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SplitLog(ctx context.Context, input graphcore.LogSplitInput) (*models.Log, error) {
|
||||
|
||||
@@ -132,6 +132,9 @@ input LogImportInput {
|
||||
"The date of the log, if not provided in the log body."
|
||||
date: Time
|
||||
|
||||
"Session threwhold"
|
||||
sessionThresholdMs: Int
|
||||
|
||||
"The log body itself."
|
||||
data: String!
|
||||
}
|
||||
@@ -171,4 +174,9 @@ enum LogImporter {
|
||||
Forum log: This importer parses the format on the forum. The displayed log, not the post source.
|
||||
"""
|
||||
ForumLog
|
||||
|
||||
"""
|
||||
IRCCloud: This importer parses irccloud exports.
|
||||
"""
|
||||
IrcCloud
|
||||
}
|
||||
@@ -14,12 +14,14 @@ const (
|
||||
LogImporterMircLike LogImporter = "MircLike"
|
||||
// LogImporterForumLog is a value of LogImporter
|
||||
LogImporterForumLog LogImporter = "ForumLog"
|
||||
// LogImporterForumLog is a value of LogImporter
|
||||
LogImporterIrcCloud LogImporter = "IrcCloud"
|
||||
)
|
||||
|
||||
// IsValid returns true if the underlying string is one of the correct values.
|
||||
func (e LogImporter) IsValid() bool {
|
||||
switch e {
|
||||
case LogImporterForumLog, LogImporterMircLike:
|
||||
case LogImporterForumLog, LogImporterMircLike, LogImporterIrcCloud:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
+67
-15
@@ -112,7 +112,7 @@ func (s *LogService) Create(ctx context.Context, title, description, channelName
|
||||
}
|
||||
|
||||
// Import creates new logs from common formats.
|
||||
func (s *LogService) Import(ctx context.Context, importer models.LogImporter, date time.Time, tz *time.Location, channelName string, data string) ([]*models.Log, error) {
|
||||
func (s *LogService) Import(ctx context.Context, importer models.LogImporter, date time.Time, tz *time.Location, channelName string, sessionThreshold time.Duration, data string) ([]*models.Log, error) {
|
||||
if err := s.authService.CheckPermission(ctx, "add", &models.Log{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -145,26 +145,33 @@ func (s *LogService) Import(ctx context.Context, importer models.LogImporter, da
|
||||
parsed.Log.EventName = eventName
|
||||
parsed.Log.ChannelName = channelName
|
||||
|
||||
log, err := s.logs.Insert(ctx, parsed.Log)
|
||||
newLog, err := s.logs.Insert(ctx, parsed.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, post := range parsed.Posts {
|
||||
post.LogID = log.ShortID
|
||||
post.LogID = newLog.ShortID
|
||||
}
|
||||
|
||||
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
||||
if err != nil {
|
||||
_ = s.logs.Delete(ctx, *log)
|
||||
_ = s.logs.Delete(ctx, *newLog)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
|
||||
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
|
||||
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(newLog), newLog)
|
||||
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(newLog, posts), newLog, posts)
|
||||
|
||||
results = append(results, log)
|
||||
refreshedLog, err := s.refreshLogCharacters(ctx, *newLog, nil, false)
|
||||
if err != nil {
|
||||
log.Printf("Failed to update characters in newLog %s: %s", newLog.ID, err)
|
||||
} else {
|
||||
newLog = refreshedLog
|
||||
}
|
||||
|
||||
results = append(results, newLog)
|
||||
}
|
||||
case models.LogImporterForumLog:
|
||||
{
|
||||
@@ -174,28 +181,73 @@ func (s *LogService) Import(ctx context.Context, importer models.LogImporter, da
|
||||
}
|
||||
|
||||
for _, parsed := range parseResults {
|
||||
log, err := s.logs.Insert(ctx, parsed.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsed.Log.EventName = eventName
|
||||
parsed.Log.ChannelName = channelName
|
||||
|
||||
newLog, err := s.logs.Insert(ctx, parsed.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, post := range parsed.Posts {
|
||||
post.LogID = log.ShortID
|
||||
post.LogID = newLog.ShortID
|
||||
}
|
||||
|
||||
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
||||
if err != nil {
|
||||
_ = s.logs.Delete(ctx, *log)
|
||||
_ = s.logs.Delete(ctx, *newLog)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
|
||||
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
|
||||
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(newLog), newLog)
|
||||
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(newLog, posts), newLog, posts)
|
||||
|
||||
results = append(results, newLog)
|
||||
}
|
||||
|
||||
}
|
||||
case models.LogImporterIrcCloud:
|
||||
{
|
||||
parseResults, err := parsers.IRCCloudLogs(data, tz, sessionThreshold)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, parsed := range parseResults {
|
||||
parsed.Log.EventName = eventName
|
||||
parsed.Log.ChannelName = channelName
|
||||
|
||||
newLog, err := s.logs.Insert(ctx, parsed.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, post := range parsed.Posts {
|
||||
post.LogID = newLog.ShortID
|
||||
}
|
||||
|
||||
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
||||
if err != nil {
|
||||
_ = s.logs.Delete(ctx, *newLog)
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
refreshedLog, err := s.refreshLogCharacters(ctx, *newLog, nil, false)
|
||||
if err != nil {
|
||||
log.Printf("Failed to update characters in newLog %s: %s", newLog.ID, err)
|
||||
} else {
|
||||
newLog = refreshedLog
|
||||
}
|
||||
|
||||
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(newLog), newLog)
|
||||
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(newLog, posts), newLog, posts)
|
||||
|
||||
results = append(results, newLog)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return nil, errors.New("Invalid importer: " + importer.String())
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIRCCloudPost(t *testing.T) {
|
||||
table := []struct {
|
||||
Input string
|
||||
TS string
|
||||
Kind string
|
||||
Nick string
|
||||
Text string
|
||||
Skipped bool
|
||||
}{
|
||||
{
|
||||
"[2016-07-18 12:57:17] — \u001FCharacter_Name\u001F does things.",
|
||||
"2016-07-18 12:57:17", "action", "Character_Name", "does things.",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"[2016-07-18 13:04:06] — Character keeps their mouth shut again for the time beings.",
|
||||
"2016-07-18 13:04:06", "action", "Character", "keeps their mouth shut again for the time beings.",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"[2016-07-18 12:34:42] <Victoria_Steels> \"I can keep it professional.\"",
|
||||
"2016-07-18 12:34:42", "text", "Victoria_Steels", "\"I can keep it professional.\"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"[2016-07-18 10:41:10] * UserZzz → User",
|
||||
"", "", "", "",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"[2016-07-18 12:53:55] → Someone joined",
|
||||
"", "", "", "",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"[2016-07-18 22:14:15] ⇐ SomeoneElse quit (s@example.com): Ping timeout: 547 seconds",
|
||||
"", "", "", "",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, row := range table {
|
||||
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) {
|
||||
post, err := IRCCloudPost(row.Input, time.UTC)
|
||||
if err != nil && err != ErrSkip {
|
||||
t.Fatal("Could not parse post:", err)
|
||||
}
|
||||
|
||||
if row.Skipped {
|
||||
if err == ErrSkip {
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatal("Row should be skipped")
|
||||
}
|
||||
if !row.Skipped && err == ErrSkip {
|
||||
t.Fatal("Row should not be skipped, but is")
|
||||
}
|
||||
|
||||
assert.Equal(t, row.TS, post.Time.Format("2006-01-02 15:04:05"), "Timestamps should match.")
|
||||
assert.Equal(t, row.Kind, post.Kind, "Kinds should match.")
|
||||
assert.Equal(t, row.Nick, post.Nick, "Kinds should match.")
|
||||
assert.Equal(t, row.Text, post.Text, "Kinds should match.")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRCCloudLog(t *testing.T) {
|
||||
type logLine struct {
|
||||
TS string
|
||||
Kind string
|
||||
Nick string
|
||||
Text string
|
||||
}
|
||||
|
||||
logs := [][]logLine{
|
||||
{
|
||||
{"2011-09-23 15:37:16", "action", "Jason_Wolfe", "sheds a bit of the tension he was harboring as Markus extends his hand."},
|
||||
{"2011-09-23 15:41:15", "action", "Markus_Vasquez", "gives another nod, but there's no smile from him."},
|
||||
{"2011-09-23 15:47:22", "action", "Jason_Wolfe", "fights the temptation to shove his hand back in his pocket as he notices Markus."},
|
||||
{"2011-09-23 15:47:22", "action", "Jason_Wolfe", "warmer to wear."},
|
||||
{"2011-09-23 15:47:32", "text", "Dante", "((you're*))"},
|
||||
{"2011-09-23 15:48:45", "action", "Markus_Vasquez", "grunts as they walk toward the door, shaking his head slightly."},
|
||||
},
|
||||
{
|
||||
{"2011-09-25 21:03:55", "scene", "=Scene=", "It's early evening on the 11th of November, and the weather is clear."},
|
||||
{"2011-09-25 21:03:55", "scene", "=Scene=", "café on the eastern edge of town."},
|
||||
{"2011-09-25 21:08:33", "action", "Sofia_Tennhausen", "is sat on a small table outside the cafe, a cup of lukewarm coffee in her hand."},
|
||||
},
|
||||
}
|
||||
|
||||
results, err := IRCCloudLogs(testLog, time.UTC, time.Hour*6)
|
||||
if err != nil {
|
||||
t.Fatal("Parse", err)
|
||||
}
|
||||
|
||||
if len(results) != len(logs) {
|
||||
t.Fatal("Two logs expected, got", len(logs))
|
||||
}
|
||||
|
||||
for i, log := range logs {
|
||||
result := results[i]
|
||||
|
||||
if len(result.Posts) != len(log) {
|
||||
t.Error(len(log), "posts expected in log", i, "but got", len(result.Posts))
|
||||
for _, post := range result.Posts {
|
||||
t.Log(*post)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for j, post := range result.Posts {
|
||||
t.Run(fmt.Sprintf("Log_%d_Post_%d", i, j), func(t *testing.T) {
|
||||
assert.Equal(t, log[j].TS, post.Time.Format("2006-01-02 15:04:05"), "Timestamps should match.")
|
||||
assert.Equal(t, log[j].Kind, post.Kind, "Kinds should match.")
|
||||
assert.Equal(t, log[j].Nick, post.Nick, "Kinds should match.")
|
||||
assert.Equal(t, log[j].Text, post.Text, "Kinds should match.")
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var testLog = `
|
||||
[2011-09-23 15:37:16] — Jason_Wolfe sheds a bit of the tension he was harboring as Markus extends his hand.
|
||||
[2011-09-23 15:41:15] — Markus_Vasquez gives another nod, but there's no smile from him.
|
||||
|
||||
|
||||
[2011-09-23 15:47:22] — Jason_Wolfe fights the temptation to shove his hand back in his pocket as he notices Markus.
|
||||
[2011-09-23 15:47:22] — Jason_Wolfe warmer to wear.
|
||||
[2011-09-23 15:47:32] <Dante> ((you're*))
|
||||
[2011-09-23 15:48:45] — Markus_Vasquez grunts as they walk toward the door, shaking his head slightly.
|
||||
[2011-09-23 18:00:28] ⇐ Tyranniac quit (Tyranniac@example.com): Ping timeout: 260 seconds
|
||||
[2011-09-23 18:58:39] → Tyranniac joined (Tyranniac@example.com)
|
||||
[2011-09-23 18:58:39] * ChanServ set +v Tyranniac
|
||||
[2011-09-24 01:45:21] → Hobo joined (Hobo@example.com)
|
||||
[2011-09-24 11:37:07] ⇐ Hobo quit (Hobo@example.com):
|
||||
[2011-09-24 21:01:34] ⇐ Tyranniac quit (Tyranniac@example.com): Ping timeout: 264 seconds
|
||||
[2011-09-24 23:00:32] → Tyranniac joined (Tyranniac@example.com)
|
||||
[2011-09-25 20:54:43] * Bowe → Sofia_Tennhausen
|
||||
[2011-09-25 21:03:55] <=Scene=> It's early evening on the 11th of November, and the weather is clear.
|
||||
[2011-09-25 21:03:55] <=Scene=> café on the eastern edge of town.
|
||||
[2011-09-25 21:08:33] — Sofia_Tennhausen is sat on a small table outside the cafe, a cup of lukewarm coffee in her hand.
|
||||
`
|
||||
@@ -2,14 +2,26 @@ package parsers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrNotPost is returned by parsePost if the line is empty or not a post.
|
||||
var ErrNotPost = errors.New("not a post")
|
||||
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")
|
||||
@@ -56,24 +68,36 @@ 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{}, ErrNotPost
|
||||
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{}, ErrNotPost
|
||||
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{}, ErrNotPost
|
||||
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{}, ErrNotPost
|
||||
return models.Post{}, &ParseError{
|
||||
Line: line,
|
||||
Problem: "invalid number in timestamp",
|
||||
}
|
||||
}
|
||||
|
||||
tsUnits[i] = n
|
||||
@@ -94,7 +118,10 @@ func MircPost(line string, date time.Time, prev models.Post) (models.Post, error
|
||||
if line[tsEndIndex+2] == '*' {
|
||||
split := strings.SplitN(line[tsEndIndex+4:], " ", 2)
|
||||
if len(split) == 1 {
|
||||
return models.Post{}, ErrNotPost
|
||||
return models.Post{}, &ParseError{
|
||||
Line: line,
|
||||
Problem: "post is empty",
|
||||
}
|
||||
}
|
||||
|
||||
post := models.Post{
|
||||
@@ -102,7 +129,7 @@ func MircPost(line string, date time.Time, prev models.Post) (models.Post, error
|
||||
LogID: "UNASSIGNED",
|
||||
Time: ts,
|
||||
Kind: "action",
|
||||
Nick: strings.TrimLeft(split[0], "+@!~"),
|
||||
Nick: strings.Trim(strings.TrimLeft(split[0], "+@!~\u001F"), "\u001F"),
|
||||
Text: split[1],
|
||||
Position: prev.Position + 1,
|
||||
}
|
||||
@@ -115,7 +142,10 @@ func MircPost(line string, date time.Time, prev models.Post) (models.Post, error
|
||||
} else if line[tsEndIndex+2] == '<' {
|
||||
split := strings.SplitN(line[tsEndIndex+2:], " ", 2)
|
||||
if len(split) == 1 {
|
||||
return models.Post{}, ErrNotPost
|
||||
return models.Post{}, &ParseError{
|
||||
Line: line,
|
||||
Problem: "post is empty",
|
||||
}
|
||||
}
|
||||
|
||||
post := models.Post{
|
||||
@@ -123,7 +153,7 @@ func MircPost(line string, date time.Time, prev models.Post) (models.Post, error
|
||||
LogID: "UNASSIGNED",
|
||||
Time: ts,
|
||||
Kind: "text",
|
||||
Nick: strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"),
|
||||
Nick: strings.Trim(strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"), "\u001F"),
|
||||
Text: split[1],
|
||||
Position: prev.Position + 1,
|
||||
}
|
||||
@@ -134,6 +164,9 @@ func MircPost(line string, date time.Time, prev models.Post) (models.Post, error
|
||||
|
||||
return post, nil
|
||||
} else {
|
||||
return models.Post{}, ErrNotPost
|
||||
return models.Post{}, &ParseError{
|
||||
Line: line,
|
||||
Problem: "line is neither action nor text post",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ func TestMircPost(t *testing.T) {
|
||||
"[13:36:59] <Stuff> Things said.",
|
||||
"13:36:59", "text", "Stuff", "Things said.",
|
||||
},
|
||||
{
|
||||
"[13:36:59] <\u001FStuff\u001F> Things said.",
|
||||
"13:36:59", "text", "Stuff", "Things said.",
|
||||
},
|
||||
{
|
||||
"[23:59] <=Scene=> Scenery and such.",
|
||||
"23:59:00", "scene", "=Scene=", "Scenery and such.",
|
||||
@@ -58,29 +62,38 @@ func TestMircPost(t *testing.T) {
|
||||
func TestMircPostErrors(t *testing.T) {
|
||||
table := []struct {
|
||||
Input string
|
||||
Err error
|
||||
Err string
|
||||
}{
|
||||
{"[12:34] <Stuff> Things said.", nil},
|
||||
{"[12:34] >Stuff> Things said.", parsers.ErrNotPost},
|
||||
{"12:34] <Stuff> Things said.", parsers.ErrNotPost},
|
||||
{"* Stuff Things said.", parsers.ErrNotPost},
|
||||
{"", parsers.ErrNotPost},
|
||||
{"[12:34 <Stuff> Things said.", parsers.ErrNotPost},
|
||||
{"[TE:XT] <Stuff> Things said.", parsers.ErrNotPost},
|
||||
{"[10] <Stuff> Things said.", parsers.ErrNotPost},
|
||||
{"[12:34:56:789] <Stuff> Things said.", nil},
|
||||
{"[12:34:56.789] <Stuff> Things said.", parsers.ErrNotPost},
|
||||
{"[12:34] <Stuff>", parsers.ErrNotPost},
|
||||
{"[12:34] * Stuff", parsers.ErrNotPost},
|
||||
{"[12:34] =Scene=", parsers.ErrNotPost},
|
||||
{"[12:34] <=Scene=>", parsers.ErrNotPost},
|
||||
{"[12:34] <Stuff> Things said.", ""},
|
||||
{"[12:34] >Stuff> Things said.", "line is neither action nor text post"},
|
||||
{"12:34] <Stuff> Things said.", "no timestamp"},
|
||||
{"* Stuff Things said.", "no timestamp"},
|
||||
{"", "no timestamp"},
|
||||
{"[12:34 <Stuff> Things said.", "incomplete timestamp"},
|
||||
{"[TE:XT] <Stuff> Things said.", "invalid number in timestamp"},
|
||||
{"[10] <Stuff> Things said.", "invalid timestamp"},
|
||||
{"[12:34:56:789] <Stuff> Things said.", ""},
|
||||
{"[12:34:56.789] <Stuff> Things said.", "invalid number in timestamp"},
|
||||
{"[12:34] <Stuff>", "post is empty"},
|
||||
{"[12:34] * Stuff", "post is empty"},
|
||||
{"[12:34] =Scene=", "line is neither action nor text post"},
|
||||
{"[12:34] <=Scene=>", "post is empty"},
|
||||
}
|
||||
|
||||
for i, row := range table {
|
||||
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) {
|
||||
_, err := parsers.MircPost(row.Input, time.Now(), models.Post{})
|
||||
|
||||
assert.Equal(t, row.Err, err, "Error should match")
|
||||
errProblem := ""
|
||||
if err != nil {
|
||||
if e2, ok := err.(*parsers.ParseError); ok {
|
||||
errProblem = e2.Problem
|
||||
} else {
|
||||
errProblem = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, row.Err, errProblem, "Error should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -160,7 +173,7 @@ func TestMircLogErrors(t *testing.T) {
|
||||
_, err2 := parsers.MircLog("\n\n\n\n\n[14:57]* Stuff \n\t\r\n", time.Time{}, true)
|
||||
|
||||
assert.Equal(t, parsers.ErrEmptyLog, err1)
|
||||
assert.Equal(t, parsers.ErrNotPost, err2)
|
||||
assert.True(t, parsers.IsParseError(err2))
|
||||
}
|
||||
|
||||
func parseDate(t *testing.T, date string) time.Time {
|
||||
|
||||
+2
-2
@@ -265,9 +265,9 @@ func (s *StoryService) EditChapter(ctx context.Context, chapter *models.Chapter,
|
||||
}
|
||||
|
||||
if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil {
|
||||
s.changeService.Submit(ctx, "Comment", "add", story.Listed, changekeys.Many(story, chapter), chapter)
|
||||
s.changeService.Submit(ctx, "Chapter", "edit", story.Listed, changekeys.Many(story, chapter), chapter)
|
||||
} else {
|
||||
s.changeService.Submit(ctx, "Comment", "add", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter), chapter)
|
||||
s.changeService.Submit(ctx, "Chapter", "edit", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter), chapter)
|
||||
}
|
||||
|
||||
return chapter, nil
|
||||
|
||||
Reference in New Issue
Block a user