diff --git a/Gopkg.lock b/Gopkg.lock index 0b53026..ab44be4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -23,6 +23,12 @@ revision = "3d21ba515fe27b856f230847e856431ae1724adc" version = "v1.0.0" +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" + version = "v1.1.1" + [[projects]] name = "github.com/dgrijalva/jwt-go" packages = ["."] @@ -71,27 +77,6 @@ revision = "78139374585c29dcb97b8f33089ed11959e4be59" version = "v5" -[[projects]] - branch = "master" - name = "github.com/graph-gophers/graphql-go" - packages = [ - ".", - "errors", - "internal/common", - "internal/exec", - "internal/exec/packer", - "internal/exec/resolvable", - "internal/exec/selected", - "internal/query", - "internal/schema", - "internal/validation", - "introspection", - "log", - "relay", - "trace" - ] - revision = "9ebf33af539ab8cb832c7107bc0a978ca8dbc0de" - [[projects]] name = "github.com/hashicorp/golang-lru" packages = [ @@ -133,7 +118,6 @@ name = "github.com/opentracing/opentracing-go" packages = [ ".", - "ext", "log" ] revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38" @@ -145,12 +129,24 @@ revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + [[projects]] name = "github.com/sirupsen/logrus" packages = ["."] revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" version = "v1.0.5" +[[projects]] + name = "github.com/stretchr/testify" + packages = ["assert"] + revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" + version = "v1.2.2" + [[projects]] name = "github.com/urfave/cli" packages = ["."] @@ -249,6 +245,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "3c9430afa8b9260026925c8f496af934e2e573a2bd4587bb634fd12a20ce6c2c" + inputs-digest = "eb8fc099a909f5d8f756b24fbcb6cc2856e132bd3ed16b8fe512f22fabb99596" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index da8e761..5644f6e 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -40,4 +40,7 @@ required = [ "github.com/99designs/gqlgen" ] [[constraint]] branch = "master" - name = "github.com/graph-gophers/graphql-go" \ No newline at end of file + name = "github.com/graph-gophers/graphql-go" +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.2" diff --git a/internal/importers/mirclike/log.go b/internal/importers/mirclike/log.go new file mode 100644 index 0000000..935c54f --- /dev/null +++ b/internal/importers/mirclike/log.go @@ -0,0 +1,48 @@ +package mirclike + +import ( + "errors" + "strings" + "time" + + "git.aiterp.net/rpdata/api/models" +) + +// ErrEmptyLog is returned by ParseLog if there are no (valid) posts in the log. +var ErrEmptyLog = errors.New("No valid posts found in log") + +// ParseLog parses the log and returns the things that can be gleamed from them. +func ParseLog(data string, date time.Time, strict bool) (models.Log, []models.Post, 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 := ParsePost(line, date, prev) + if err != nil { + if strict { + return models.Log{}, nil, err + } + + continue + } + + posts = append(posts, post) + prev = post + } + + if len(posts) == 0 { + return models.Log{}, nil, ErrEmptyLog + } + + log := models.Log{ + Date: posts[0].Time, + } + + return log, posts, nil +} diff --git a/internal/importers/mirclike/log_test.go b/internal/importers/mirclike/log_test.go new file mode 100644 index 0000000..478ef5b --- /dev/null +++ b/internal/importers/mirclike/log_test.go @@ -0,0 +1,69 @@ +package mirclike_test + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "git.aiterp.net/rpdata/api/internal/importers/mirclike" + "git.aiterp.net/rpdata/api/models" +) + +var testLog = strings.Join([]string{ + "[11:21] * Va`ynna_Atana returns to the apartment at the end of another long day. She hangs up her jacket and a green scarf next to the door before going straight to the bathroom; she leaves the door open, though. She walked home with Uvena this time, but the two parted ways downstairs.", + "", + "[11:21] <=Scene=> The weather outside is not pleasant in the slightest. The storm is still going on, visibility is reduced and the worst gusts of wind can be felt inside the apartment as a faint shudder. ", + "", + "[11:", + " [11:27] Stuff and things.", +}, "\r\n") + +var testLogPosts = []models.Post{ + { + ID: "UNASSIGNED", + LogID: "UNASSIGNED", + Time: parseDate(nil, "2018-05-11 11:21:00"), + Kind: "action", + Nick: "Va`ynna_Atana", + Position: 1, + Text: "returns to the apartment at the end of another long day. She hangs up her jacket and a green scarf next to the door before going straight to the bathroom; she leaves the door open, though. She walked home with Uvena this time, but the two parted ways downstairs.", + }, + { + ID: "UNASSIGNED", + LogID: "UNASSIGNED", + Time: parseDate(nil, "2018-05-11 11:21:00"), + Kind: "scene", + Nick: "=Scene=", + Position: 2, + Text: "The weather outside is not pleasant in the slightest. The storm is still going on, visibility is reduced and the worst gusts of wind can be felt inside the apartment as a faint shudder.", + }, + { + ID: "UNASSIGNED", + LogID: "UNASSIGNED", + Time: parseDate(nil, "2018-05-11 11:27:00"), + Kind: "text", + Nick: "Test", + Position: 3, + Text: "Stuff and things.", + }, +} + +func TestParseLog(t *testing.T) { + log, posts, err := mirclike.ParseLog(testLog, parseDate(t, "2018-05-11 00:00:00"), false) + if err != nil { + t.Fatal("ParseLog", err) + } + + assert.Equal(t, testLogPosts, posts) + assert.Equal(t, posts[0].Time, log.Date, "Log's date should be the first post's.") +} + +func TestParseLogErrors(t *testing.T) { + _, _, err1 := mirclike.ParseLog("\n\n\n\n\n \n\t\r\n", time.Time{}, false) + _, _, err2 := mirclike.ParseLog("\n\n\n\n\n[14:57]* Stuff \n\t\r\n", time.Time{}, true) + + assert.Equal(t, mirclike.ErrEmptyLog, err1) + assert.Equal(t, mirclike.ErrNotPost, err2) +} diff --git a/internal/importers/mirclike/post.go b/internal/importers/mirclike/post.go new file mode 100644 index 0000000..c2978ca --- /dev/null +++ b/internal/importers/mirclike/post.go @@ -0,0 +1,92 @@ +package mirclike + +import ( + "errors" + "strconv" + "strings" + "time" + + "git.aiterp.net/rpdata/api/models" +) + +// ErrNotPost is returned by parsePost if the line is empty or not a post. +var ErrNotPost = errors.New("not a post") + +// ParsePost 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 ParsePost(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 + } + + // Parse timestamp + tsEndIndex := strings.IndexByte(line, ']') + if tsEndIndex == -1 || len(line) < tsEndIndex+5 { + return models.Post{}, ErrNotPost + } + tsStr := line[1:tsEndIndex] + tsSplit := strings.Split(tsStr, ":") + tsUnits := make([]int, len(tsSplit)) + if len(tsSplit) < 2 { + return models.Post{}, ErrNotPost + } + for i := range tsSplit { + n, err := strconv.Atoi(tsSplit[i]) + if err != nil { + return models.Post{}, ErrNotPost + } + + tsUnits[i] = n + } + if len(tsUnits) == 2 { + tsUnits = append(tsUnits, 0) + } + + // Determine timestamp from parsed data and previous post. + ts := time.Date(date.Year(), date.Month(), date.Day(), tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location()) + if !prev.Time.IsZero() && prev.Time.Sub(ts) > 30*time.Minute { + ts = time.Date(prev.Time.Year(), prev.Time.Month(), prev.Time.Day()+1, tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location()) + } + + if line[tsEndIndex+2] == '*' { + split := strings.SplitN(line[tsEndIndex+4:], " ", 2) + + post := models.Post{ + ID: "UNASSIGNED", + LogID: "UNASSIGNED", + Time: ts, + Kind: "action", + Nick: strings.TrimLeft(split[0], "+@!~"), + 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) + + post := models.Post{ + ID: "UNASSIGNED", + LogID: "UNASSIGNED", + Time: ts, + Kind: "text", + Nick: strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"), + Text: split[1], + Position: prev.Position + 1, + } + + if post.Nick[0] == '=' { + post.Kind = "scene" + } + + return post, nil + } else { + return models.Post{}, ErrNotPost + } +} diff --git a/internal/importers/mirclike/post_test.go b/internal/importers/mirclike/post_test.go new file mode 100644 index 0000000..c88a86e --- /dev/null +++ b/internal/importers/mirclike/post_test.go @@ -0,0 +1,126 @@ +package mirclike_test + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "git.aiterp.net/rpdata/api/internal/importers/mirclike" + "git.aiterp.net/rpdata/api/models" +) + +func TestParsePost(t *testing.T) { + table := []struct { + Input string + TS string + Kind string + Nick string + Text string + }{ + { + "[12:34] * Stuff does things.", + "12:34:00", "action", "Stuff", "does things.", + }, + { + "[12:34] Things said.", + "12:34:00", "text", "Stuff", "Things said.", + }, + { + "[13:36:59] Things said.", + "13:36:59", "text", "Stuff", "Things said.", + }, + { + "[23:59] <=Scene=> Scenery and such.", + "23:59:00", "scene", "=Scene=", "Scenery and such.", + }, + { + "[01:10:11] * =Scene= Scenery and such from the forum or mIRC using my old script.", + "01:10:11", "scene", "=Scene=", "Scenery and such from the forum or mIRC using my old script.", + }, + } + + for i, row := range table { + t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) { + post, err := mirclike.ParsePost(row.Input, time.Now(), models.Post{}) + if err != nil { + t.Fatal("Could not parse post:", err) + } + + assert.Equal(t, row.TS, post.Time.Format("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 TestParsePostErrors(t *testing.T) { + table := []struct { + Input string + Err error + }{ + {"[12:34] Things said.", nil}, + {"[12:34] >Stuff> Things said.", mirclike.ErrNotPost}, + {"12:34] Things said.", mirclike.ErrNotPost}, + {"* Stuff Things said.", mirclike.ErrNotPost}, + {"", mirclike.ErrNotPost}, + {"[12:34 Things said.", mirclike.ErrNotPost}, + {"[TE:XT] Things said.", mirclike.ErrNotPost}, + {"[10] Things said.", mirclike.ErrNotPost}, + {"[12:34:56:789] Things said.", nil}, + {"[12:34:56.789] Things said.", mirclike.ErrNotPost}, + } + + for i, row := range table { + t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) { + _, err := mirclike.ParsePost(row.Input, time.Now(), models.Post{}) + + assert.Equal(t, row.Err, err, "Error should match") + }) + } +} + +func TestParseNextDay(t *testing.T) { + table := []struct { + Prev time.Time + TS string + Time time.Time + }{ + {Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "12:01", Time: parseDate(t, "2019-01-12 12:01:00")}, + {Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "11:53:13", Time: parseDate(t, "2019-01-12 11:53:13")}, + {Prev: parseDate(t, "2019-04-08 23:51:59"), TS: "00:09", Time: parseDate(t, "2019-04-09 00:09:00")}, + {Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "11:29:59", Time: parseDate(t, "2019-01-13 11:29:59")}, + } + + for i, row := range table { + t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) { + input := fmt.Sprintf("[%s] * Stuff does things.", row.TS) + + post, err := mirclike.ParsePost(input, row.Prev, models.Post{Time: row.Prev}) + if err != nil { + t.Fatal("Could not parse post:", err) + } + + assert.Equal(t, row.Time, post.Time) + }) + } +} + +func parseDate(t *testing.T, date string) time.Time { + result, err := time.Parse("2006-01-02 15:04:05", date) + if err != nil { + if t != nil { + t.Fatal("Could not parse date", date, err) + } else { + panic("Could not parse date: " + err.Error()) + } + } + + return result +} + +func formatDate(date time.Time) string { + return date.UTC().Format("2006-01-02 15:04:05") +}