diff --git a/cmd/rpdata-server/main.go b/cmd/rpdata-server/main.go index 4e8686e..e1f59ba 100644 --- a/cmd/rpdata-server/main.go +++ b/cmd/rpdata-server/main.go @@ -8,6 +8,8 @@ import ( "runtime/debug" "strings" + "git.aiterp.net/rpdata/api/models/logs" + "git.aiterp.net/rpdata/api/graph2" "git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/internal/loader" @@ -24,6 +26,15 @@ func main() { http.Handle("/", handler.Playground("RPData API", "/graphql")) http.Handle("/graphql", queryHandler()) + go func() { + err := logs.RunFullUpdate() + if err != nil { + log.Println(err) + } + + log.Println("Characters updated") + }() + log.Fatal(http.ListenAndServe(":8081", nil)) } diff --git a/graph2/queries/character.go b/graph2/queries/character.go index 3e5dde3..d45d9d0 100644 --- a/graph2/queries/character.go +++ b/graph2/queries/character.go @@ -9,6 +9,7 @@ import ( "git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/models/characters" + "git.aiterp.net/rpdata/api/models/logs" ) // Queries @@ -59,6 +60,8 @@ func (r *mutationResolver) AddCharacter(ctx context.Context, input input.Charact author = *input.Author } + logs.ScheduleFullUpdate() + return characters.Add(input.Nick, input.Name, shortName, author, description) } @@ -77,6 +80,8 @@ func (r *mutationResolver) AddCharacterNick(ctx context.Context, input input.Cha return models.Character{}, errors.New("You are not permitted to edit this character") } + logs.ScheduleFullUpdate() + return characters.AddNick(character, input.Nick) } @@ -91,6 +96,8 @@ func (r *mutationResolver) RemoveCharacterNick(ctx context.Context, input input. return models.Character{}, errors.New("You are not permitted to edit this character") } + logs.ScheduleFullUpdate() + return characters.RemoveNick(character, input.Nick) } diff --git a/graph2/queries/post.go b/graph2/queries/post.go index 570cc07..86d3797 100644 --- a/graph2/queries/post.go +++ b/graph2/queries/post.go @@ -52,7 +52,14 @@ func (r *mutationResolver) AddPost(ctx context.Context, input input.PostAddInput return models.Post{}, err } - return posts.Add(log, input.Time, input.Kind, input.Nick, input.Text) + post, err := posts.Add(log, input.Time, input.Kind, input.Nick, input.Text) + if err != nil { + return models.Post{}, err + } + + go logs.UpdateCharacters(log) + + return post, nil } func (r *mutationResolver) EditPost(ctx context.Context, input input.PostEditInput) (models.Post, error) { @@ -66,6 +73,17 @@ func (r *mutationResolver) EditPost(ctx context.Context, input input.PostEditInp return models.Post{}, errors.New("Post not found") } + if input.Nick != nil { + go func() { + log, err := logs.FindID(post.LogID) + if err != nil { + return + } + + logs.UpdateCharacters(log) + }() + } + return posts.Edit(post, input.Time, input.Kind, input.Nick, input.Text) } @@ -94,5 +112,14 @@ func (r *mutationResolver) RemovePost(ctx context.Context, input input.PostRemov return models.Post{}, errors.New("Post not found (before removing, of course)") } + go func() { + log, err := logs.FindID(post.LogID) + if err != nil { + return + } + + logs.UpdateCharacters(log) + }() + return posts.Remove(post) } diff --git a/internal/task/task.go b/internal/task/task.go new file mode 100644 index 0000000..bd67894 --- /dev/null +++ b/internal/task/task.go @@ -0,0 +1,84 @@ +package task + +import ( + "context" + "sync" + "time" +) + +var globalCtx, globalCancel = context.WithCancel(context.Background()) + +// A Task is a wrapper around a function that offers scheduling +// and syncronization. +type Task struct { + mutex sync.Mutex + ctx context.Context + ctxCancel context.CancelFunc + waitDuration time.Duration + scheduled bool + callback func() error +} + +// Context gets the task's context. +func (task *Task) Context() context.Context { + return task.ctx +} + +// Stop stops a task. This should not be done to stop a single run, +// but as a way to cancel it forever. +func (task *Task) Stop() { + task.ctxCancel() +} + +// Schedule schedules a task for later execution. +func (task *Task) Schedule() { + // Don't if the context is closed. + select { + case <-task.Context().Done(): + return + default: + } + + task.mutex.Lock() + if task.scheduled { + task.mutex.Unlock() + return + } + task.scheduled = true + task.mutex.Unlock() + + go func() { + <-time.After(task.waitDuration) + + // Mark as no longer scheduled + task.mutex.Lock() + task.scheduled = false + task.mutex.Unlock() + + // Don't if the task is cancelled + select { + case <-task.Context().Done(): + return + default: + } + + task.callback() + }() +} + +// New makes a new task with the callback. +func New(waitDuration time.Duration, callback func() error) *Task { + ctx, ctxCancel := context.WithCancel(globalCtx) + + return &Task{ + callback: callback, + ctx: ctx, + ctxCancel: ctxCancel, + waitDuration: waitDuration, + } +} + +// StopAll stops all tasks. +func StopAll() { + globalCancel() +} diff --git a/models/characters/list.go b/models/characters/list.go index 9b72798..5820be2 100644 --- a/models/characters/list.go +++ b/models/characters/list.go @@ -25,10 +25,10 @@ func List(filter *Filter) ([]models.Character, error) { query["_id"] = filter.IDs[0] } - if len(filter.Nicks) > 1 { - query["nicks"] = bson.M{"$in": filter.Nicks} - } else if len(filter.Nicks) == 1 { + if len(filter.Nicks) == 1 { query["nicks"] = filter.Nicks[0] + } else if filter.Nicks != nil { + query["nicks"] = bson.M{"$in": filter.Nicks} } if len(filter.Names) > 1 { diff --git a/models/logs/update-characters.go b/models/logs/update-characters.go new file mode 100644 index 0000000..b665853 --- /dev/null +++ b/models/logs/update-characters.go @@ -0,0 +1,106 @@ +package logs + +import ( + "strings" + "time" + + "git.aiterp.net/rpdata/api/internal/task" + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/models/characters" + "git.aiterp.net/rpdata/api/models/posts" + "github.com/globalsign/mgo/bson" +) + +var updateTask = task.New(time.Second*60, RunFullUpdate) + +// UpdateCharacters updates the characters for the given log. +func UpdateCharacters(log models.Log) (models.Log, error) { + posts, err := posts.List(&posts.Filter{LogID: &log.ShortID, Kind: []string{"action", "text", "chars"}, Limit: 0}) + if err != nil { + return models.Log{}, err + } + + added := make(map[string]bool) + removed := make(map[string]bool) + for _, post := range posts { + if post.Kind == "text" || post.Kind == "action" { + if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") { + continue + } + + // Clean up the nick (remove possessive suffix, comma, formatting stuff) + if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") { + post.Nick = post.Nick[:len(post.Nick)-2] + } else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") { + post.Nick = post.Nick[:len(post.Nick)-1] + } + + added[post.Nick] = true + } + if post.Kind == "chars" { + tokens := strings.Fields(post.Text) + for _, token := range tokens { + if strings.HasPrefix(token, "-") { + removed[token[1:]] = true + } else { + added[strings.Replace(token, "+", "", 1)] = true + } + } + } + } + + nicks := make([]string, 0, len(added)) + for nick := range added { + if added[nick] && !removed[nick] { + nicks = append(nicks, nick) + } + } + + characters, err := characters.List(&characters.Filter{Nicks: nicks}) + if err != nil { + return models.Log{}, err + } + + characterIDs := make([]string, len(characters)) + for i, char := range characters { + characterIDs[i] = char.ID + } + + err = collection.UpdateId(log.ID, bson.M{"$set": bson.M{"characterIds": characterIDs}}) + if err != nil { + return models.Log{}, err + } + + log.CharacterIDs = characterIDs + + return log, nil +} + +// RunFullUpdate runs a full update on all logs. +func RunFullUpdate() error { + iter := iter(bson.M{}, 0) + err := iter.Err() + if err != nil { + return err + } + + log := models.Log{} + for iter.Next(&log) { + _, err = UpdateCharacters(log) + if err != nil { + return err + } + } + + err = iter.Err() + if err != nil { + return err + } + + return nil +} + +// ScheduleFullUpdate runs a full character update within the next 60 seconds. +func ScheduleFullUpdate() { + updateTask.Schedule() +} diff --git a/test.prof b/test.prof new file mode 100644 index 0000000..72784d4 Binary files /dev/null and b/test.prof differ