Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ad0b9d467 | ||
|
|
63bc262091 | ||
|
|
c4eabd69ac | ||
|
|
df982fac17 | ||
|
|
6fa2dc9800 | ||
|
|
6ce38d61b1 |
@@ -10,6 +10,9 @@ rpdata-*
|
|||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
/main
|
||||||
|
/main.exe
|
||||||
|
|
||||||
/graph2/graphcore/*_gen.go
|
/graph2/graphcore/*_gen.go
|
||||||
generated.gql
|
generated.gql
|
||||||
generated.go
|
generated.go
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ func (r *storyRepository) Update(ctx context.Context, story models.Story, update
|
|||||||
updateBson["listed"] = *update.Listed
|
updateBson["listed"] = *update.Listed
|
||||||
story.Listed = *update.Listed
|
story.Listed = *update.Listed
|
||||||
}
|
}
|
||||||
|
if update.SortByFictionalDate != nil {
|
||||||
|
updateBson["sortByFictionalDate"] = *update.SortByFictionalDate
|
||||||
|
story.SortByFictionalDate = *update.SortByFictionalDate
|
||||||
|
}
|
||||||
|
|
||||||
err := r.stories.UpdateId(story.ID, bson.M{"$set": updateBson})
|
err := r.stories.UpdateId(story.ID, bson.M{"$set": updateBson})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ func (r *mutationResolver) ImportLog(ctx context.Context, input graphcore.LogImp
|
|||||||
tz = parsedTZ
|
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) {
|
func (r *mutationResolver) SplitLog(ctx context.Context, input graphcore.LogSplitInput) (*models.Log, error) {
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ func (r *mutationResolver) AddStory(ctx context.Context, input graphcore.StoryAd
|
|||||||
fictionalDate = *input.FictionalDate
|
fictionalDate = *input.FictionalDate
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.s.Stories.CreateStory(ctx, input.Name, input.Author, input.Category, listed, open, tags, time.Now(), fictionalDate)
|
sortByFictionalDate := input.SortByFictionalDate != nil && *input.SortByFictionalDate
|
||||||
|
|
||||||
|
return r.s.Stories.CreateStory(ctx, input.Name, input.Author, input.Category, listed, open, tags, time.Now(), fictionalDate, sortByFictionalDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) AddStoryTag(ctx context.Context, input graphcore.StoryTagAddInput) (*models.Story, error) {
|
func (r *mutationResolver) AddStoryTag(ctx context.Context, input graphcore.StoryTagAddInput) (*models.Story, error) {
|
||||||
@@ -63,7 +65,7 @@ func (r *mutationResolver) EditStory(ctx context.Context, input graphcore.StoryE
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.s.Stories.EditStory(ctx, story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate)
|
return r.s.Stories.EditStory(ctx, story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate, input.SortByFictionalDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) RemoveStory(ctx context.Context, input graphcore.StoryRemoveInput) (*models.Story, error) {
|
func (r *mutationResolver) RemoveStory(ctx context.Context, input graphcore.StoryRemoveInput) (*models.Story, error) {
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ input LogImportInput {
|
|||||||
"The date of the log, if not provided in the log body."
|
"The date of the log, if not provided in the log body."
|
||||||
date: Time
|
date: Time
|
||||||
|
|
||||||
|
"Session threwhold"
|
||||||
|
sessionThresholdMs: Int
|
||||||
|
|
||||||
"The log body itself."
|
"The log body itself."
|
||||||
data: String!
|
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.
|
Forum log: This importer parses the format on the forum. The displayed log, not the post source.
|
||||||
"""
|
"""
|
||||||
ForumLog
|
ForumLog
|
||||||
|
|
||||||
|
"""
|
||||||
|
IRCCloud: This importer parses irccloud exports.
|
||||||
|
"""
|
||||||
|
IrcCloud
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,9 @@ type Story {
|
|||||||
|
|
||||||
# The date of the last major update to the story. This being bumped means a new chapter has been added.
|
# The date of the last major update to the story. This being bumped means a new chapter has been added.
|
||||||
updatedDate: Time!
|
updatedDate: Time!
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Filter for stories query.
|
# Filter for stories query.
|
||||||
@@ -86,6 +89,9 @@ input StoryAddInput {
|
|||||||
|
|
||||||
# Set the fictional date of the story.
|
# Set the fictional date of the story.
|
||||||
fictionalDate: Time
|
fictionalDate: Time
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for the editStory mutation
|
# Input for the editStory mutation
|
||||||
@@ -115,6 +121,9 @@ input StoryEditInput {
|
|||||||
|
|
||||||
# Clear the fictional date of the story.
|
# Clear the fictional date of the story.
|
||||||
clearFictionalDate: Boolean
|
clearFictionalDate: Boolean
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for the addStoryTag mutation
|
# Input for the addStoryTag mutation
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ const (
|
|||||||
LogImporterMircLike LogImporter = "MircLike"
|
LogImporterMircLike LogImporter = "MircLike"
|
||||||
// LogImporterForumLog is a value of LogImporter
|
// LogImporterForumLog is a value of LogImporter
|
||||||
LogImporterForumLog LogImporter = "ForumLog"
|
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.
|
// IsValid returns true if the underlying string is one of the correct values.
|
||||||
func (e LogImporter) IsValid() bool {
|
func (e LogImporter) IsValid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case LogImporterForumLog, LogImporterMircLike:
|
case LogImporterForumLog, LogImporterMircLike, LogImporterIrcCloud:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
+19
-17
@@ -5,16 +5,17 @@ import "time"
|
|||||||
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
|
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
|
||||||
// The story model is a container for multiple chapters this time, in contrast to the previous version.
|
// The story model is a container for multiple chapters this time, in contrast to the previous version.
|
||||||
type Story struct {
|
type Story struct {
|
||||||
ID string `bson:"_id"`
|
ID string `bson:"_id"`
|
||||||
Author string `bson:"author"`
|
Author string `bson:"author"`
|
||||||
Name string `bson:"name"`
|
Name string `bson:"name"`
|
||||||
Category StoryCategory `bson:"category"`
|
Category StoryCategory `bson:"category"`
|
||||||
Open bool `bson:"open"`
|
Open bool `bson:"open"`
|
||||||
Listed bool `bson:"listed"`
|
Listed bool `bson:"listed"`
|
||||||
Tags []Tag `bson:"tags"`
|
Tags []Tag `bson:"tags"`
|
||||||
CreatedDate time.Time `bson:"createdDate"`
|
CreatedDate time.Time `bson:"createdDate"`
|
||||||
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
|
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
|
||||||
UpdatedDate time.Time `bson:"updatedDate"`
|
UpdatedDate time.Time `bson:"updatedDate"`
|
||||||
|
SortByFictionalDate bool `bson:"sortByFictionalDate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsChangeObject is an interface implementation to identify it as a valid
|
// IsChangeObject is an interface implementation to identify it as a valid
|
||||||
@@ -35,11 +36,12 @@ type StoryFilter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StoryUpdate struct {
|
type StoryUpdate struct {
|
||||||
Name *string
|
Name *string
|
||||||
Category *StoryCategory
|
Category *StoryCategory
|
||||||
Author *string
|
Author *string
|
||||||
Open *bool
|
Open *bool
|
||||||
Listed *bool
|
Listed *bool
|
||||||
FictionalDate *time.Time
|
FictionalDate *time.Time
|
||||||
UpdatedDate *time.Time
|
UpdatedDate *time.Time
|
||||||
|
SortByFictionalDate *bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import (
|
|||||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||||
"git.aiterp.net/rpdata/api/repositories"
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
"git.aiterp.net/rpdata/api/services/loaders"
|
"git.aiterp.net/rpdata/api/services/loaders"
|
||||||
|
"log"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CharacterService struct {
|
type CharacterService struct {
|
||||||
@@ -16,6 +19,9 @@ type CharacterService struct {
|
|||||||
loader *loaders.CharacterLoader
|
loader *loaders.CharacterLoader
|
||||||
changeService *ChangeService
|
changeService *ChangeService
|
||||||
authService *AuthService
|
authService *AuthService
|
||||||
|
logService *LogService
|
||||||
|
|
||||||
|
refreshSoftLock uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find uses the loader to find the character by the ID.
|
// Find uses the loader to find the character by the ID.
|
||||||
@@ -61,6 +67,8 @@ func (s *CharacterService) List(ctx context.Context, filter models.CharacterFilt
|
|||||||
return characters, nil
|
return characters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go s.refreshLogs()
|
||||||
|
|
||||||
return s.characters.List(ctx, filter)
|
return s.characters.List(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +116,8 @@ func (s *CharacterService) Create(ctx context.Context, nick, name, shortName, au
|
|||||||
|
|
||||||
s.changeService.Submit(ctx, "Character", "add", true, changekeys.Listed(character), character)
|
s.changeService.Submit(ctx, "Character", "add", true, changekeys.Listed(character), character)
|
||||||
|
|
||||||
|
go s.refreshLogs()
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +170,8 @@ func (s *CharacterService) AddNick(ctx context.Context, id string, nick string)
|
|||||||
|
|
||||||
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
||||||
|
|
||||||
|
go s.refreshLogs()
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +196,8 @@ func (s *CharacterService) RemoveNick(ctx context.Context, id string, nick strin
|
|||||||
|
|
||||||
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
||||||
|
|
||||||
|
go s.refreshLogs()
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,5 +221,21 @@ func (s *CharacterService) Delete(ctx context.Context, id string) (*models.Chara
|
|||||||
|
|
||||||
s.changeService.Submit(ctx, "Character", "remove", true, changekeys.Listed(character), character)
|
s.changeService.Submit(ctx, "Character", "remove", true, changekeys.Listed(character), character)
|
||||||
|
|
||||||
|
go s.refreshLogs()
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *CharacterService) refreshLogs() {
|
||||||
|
if !atomic.CompareAndSwapUint32(&s.refreshSoftLock, 0, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 60)
|
||||||
|
atomic.StoreUint32(&s.refreshSoftLock, 0)
|
||||||
|
|
||||||
|
err := s.logService.RefreshAllLogCharacters(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to refersh log characters:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+67
-15
@@ -112,7 +112,7 @@ func (s *LogService) Create(ctx context.Context, title, description, channelName
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Import creates new logs from common formats.
|
// 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 {
|
if err := s.authService.CheckPermission(ctx, "add", &models.Log{}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -145,26 +145,33 @@ func (s *LogService) Import(ctx context.Context, importer models.LogImporter, da
|
|||||||
parsed.Log.EventName = eventName
|
parsed.Log.EventName = eventName
|
||||||
parsed.Log.ChannelName = channelName
|
parsed.Log.ChannelName = channelName
|
||||||
|
|
||||||
log, err := s.logs.Insert(ctx, parsed.Log)
|
newLog, err := s.logs.Insert(ctx, parsed.Log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, post := range parsed.Posts {
|
for _, post := range parsed.Posts {
|
||||||
post.LogID = log.ShortID
|
post.LogID = newLog.ShortID
|
||||||
}
|
}
|
||||||
|
|
||||||
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = s.logs.Delete(ctx, *log)
|
_ = s.logs.Delete(ctx, *newLog)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
|
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(newLog), newLog)
|
||||||
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
|
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:
|
case models.LogImporterForumLog:
|
||||||
{
|
{
|
||||||
@@ -174,28 +181,73 @@ func (s *LogService) Import(ctx context.Context, importer models.LogImporter, da
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, parsed := range parseResults {
|
for _, parsed := range parseResults {
|
||||||
log, err := s.logs.Insert(ctx, parsed.Log)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
parsed.Log.EventName = eventName
|
parsed.Log.EventName = eventName
|
||||||
parsed.Log.ChannelName = channelName
|
parsed.Log.ChannelName = channelName
|
||||||
|
|
||||||
|
newLog, err := s.logs.Insert(ctx, parsed.Log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for _, post := range parsed.Posts {
|
for _, post := range parsed.Posts {
|
||||||
post.LogID = log.ShortID
|
post.LogID = newLog.ShortID
|
||||||
}
|
}
|
||||||
|
|
||||||
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = s.logs.Delete(ctx, *log)
|
_ = s.logs.Delete(ctx, *newLog)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
|
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(newLog), newLog)
|
||||||
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
|
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:
|
default:
|
||||||
{
|
{
|
||||||
return nil, errors.New("Invalid importer: " + importer.String())
|
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.
|
||||||
|
`
|
||||||
@@ -129,7 +129,7 @@ func MircPost(line string, date time.Time, prev models.Post) (models.Post, error
|
|||||||
LogID: "UNASSIGNED",
|
LogID: "UNASSIGNED",
|
||||||
Time: ts,
|
Time: ts,
|
||||||
Kind: "action",
|
Kind: "action",
|
||||||
Nick: strings.TrimLeft(split[0], "+@!~"),
|
Nick: strings.Trim(strings.TrimLeft(split[0], "+@!~\u001F"), "\u001F"),
|
||||||
Text: split[1],
|
Text: split[1],
|
||||||
Position: prev.Position + 1,
|
Position: prev.Position + 1,
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ func MircPost(line string, date time.Time, prev models.Post) (models.Post, error
|
|||||||
LogID: "UNASSIGNED",
|
LogID: "UNASSIGNED",
|
||||||
Time: ts,
|
Time: ts,
|
||||||
Kind: "text",
|
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],
|
Text: split[1],
|
||||||
Position: prev.Position + 1,
|
Position: prev.Position + 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ func TestMircPost(t *testing.T) {
|
|||||||
"[13:36:59] <Stuff> Things said.",
|
"[13:36:59] <Stuff> Things said.",
|
||||||
"13:36:59", "text", "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] <=Scene=> Scenery and such.",
|
||||||
"23:59:00", "scene", "=Scene=", "Scenery and such.",
|
"23:59:00", "scene", "=Scene=", "Scenery and such.",
|
||||||
|
|||||||
@@ -67,5 +67,7 @@ func NewBundle(db database.Database, spaceClient *space.Client) *Bundle {
|
|||||||
authService: bundle.Auth,
|
authService: bundle.Auth,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bundle.Characters.logService = bundle.Logs
|
||||||
|
|
||||||
return bundle
|
return bundle
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-19
@@ -7,6 +7,7 @@ import (
|
|||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||||
"git.aiterp.net/rpdata/api/repositories"
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,14 +38,37 @@ func (s *StoryService) ListStories(ctx context.Context, filter models.StoryFilte
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoryService) ListChapters(ctx context.Context, story models.Story) ([]*models.Chapter, error) {
|
func (s *StoryService) ListChapters(ctx context.Context, story models.Story) ([]*models.Chapter, error) {
|
||||||
return s.chapters.List(ctx, models.ChapterFilter{StoryID: &story.ID, Limit: 0})
|
chapters, err := s.chapters.List(ctx, models.ChapterFilter{StoryID: &story.ID, Limit: 0})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if story.SortByFictionalDate {
|
||||||
|
sort.Slice(chapters, func(i, j int) bool {
|
||||||
|
if !chapters[i].FictionalDate.IsZero() && !chapters[j].FictionalDate.IsZero() {
|
||||||
|
if chapters[i].FictionalDate.Equal(chapters[j].FictionalDate) {
|
||||||
|
return chapters[i].CreatedDate.Before(chapters[j].CreatedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters[i].FictionalDate.Before(chapters[j].FictionalDate)
|
||||||
|
} else if chapters[i].FictionalDate.IsZero() && !chapters[j].FictionalDate.IsZero() {
|
||||||
|
return false
|
||||||
|
} else if !chapters[i].FictionalDate.IsZero() && chapters[j].FictionalDate.IsZero() {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return chapters[i].CreatedDate.Before(chapters[j].CreatedDate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoryService) ListComments(ctx context.Context, chapter models.Chapter, limit int) ([]*models.Comment, error) {
|
func (s *StoryService) ListComments(ctx context.Context, chapter models.Chapter, limit int) ([]*models.Comment, error) {
|
||||||
return s.comments.List(ctx, models.CommentFilter{ChapterID: &chapter.ID, Limit: limit})
|
return s.comments.List(ctx, models.CommentFilter{ChapterID: &chapter.ID, Limit: limit})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoryService) CreateStory(ctx context.Context, name string, author *string, category models.StoryCategory, listed, open bool, tags []models.Tag, createdDate, fictionalDate time.Time) (*models.Story, error) {
|
func (s *StoryService) CreateStory(ctx context.Context, name string, author *string, category models.StoryCategory, listed, open bool, tags []models.Tag, createdDate, fictionalDate time.Time, sortByFictionalDate bool) (*models.Story, error) {
|
||||||
if author == nil {
|
if author == nil {
|
||||||
token := s.authService.TokenFromContext(ctx)
|
token := s.authService.TokenFromContext(ctx)
|
||||||
if token == nil {
|
if token == nil {
|
||||||
@@ -55,15 +79,16 @@ func (s *StoryService) CreateStory(ctx context.Context, name string, author *str
|
|||||||
}
|
}
|
||||||
|
|
||||||
story := &models.Story{
|
story := &models.Story{
|
||||||
Name: name,
|
Name: name,
|
||||||
Author: *author,
|
Author: *author,
|
||||||
Category: category,
|
Category: category,
|
||||||
Listed: listed,
|
Listed: listed,
|
||||||
Open: open,
|
Open: open,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
CreatedDate: createdDate,
|
CreatedDate: createdDate,
|
||||||
FictionalDate: fictionalDate,
|
FictionalDate: fictionalDate,
|
||||||
UpdatedDate: createdDate,
|
UpdatedDate: createdDate,
|
||||||
|
SortByFictionalDate: sortByFictionalDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.authService.CheckPermission(ctx, "add", story); err != nil {
|
if err := s.authService.CheckPermission(ctx, "add", story); err != nil {
|
||||||
@@ -180,7 +205,7 @@ func (s *StoryService) CreateComment(ctx context.Context, chapter models.Chapter
|
|||||||
return comment, nil
|
return comment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoryService) EditStory(ctx context.Context, story *models.Story, name *string, category *models.StoryCategory, listed, open *bool, fictionalDate *time.Time) (*models.Story, error) {
|
func (s *StoryService) EditStory(ctx context.Context, story *models.Story, name *string, category *models.StoryCategory, listed, open *bool, fictionalDate *time.Time, sortByFictionalDate *bool) (*models.Story, error) {
|
||||||
if story == nil {
|
if story == nil {
|
||||||
panic("StoryService.Edit called with nil story")
|
panic("StoryService.Edit called with nil story")
|
||||||
}
|
}
|
||||||
@@ -190,11 +215,12 @@ func (s *StoryService) EditStory(ctx context.Context, story *models.Story, name
|
|||||||
}
|
}
|
||||||
|
|
||||||
story, err := s.stories.Update(ctx, *story, models.StoryUpdate{
|
story, err := s.stories.Update(ctx, *story, models.StoryUpdate{
|
||||||
Name: name,
|
Name: name,
|
||||||
Open: open,
|
Open: open,
|
||||||
Listed: listed,
|
Listed: listed,
|
||||||
Category: category,
|
Category: category,
|
||||||
FictionalDate: fictionalDate,
|
FictionalDate: fictionalDate,
|
||||||
|
SortByFictionalDate: sortByFictionalDate,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -265,9 +291,9 @@ func (s *StoryService) EditChapter(ctx context.Context, chapter *models.Chapter,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil {
|
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 {
|
} 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
|
return chapter, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user