diff --git a/database/database.go b/database/database.go index d17c6a0..358b8da 100644 --- a/database/database.go +++ b/database/database.go @@ -19,6 +19,9 @@ type Database interface { Tags() repositories.TagRepository Logs() repositories.LogRepository Posts() repositories.PostRepository + Stories() repositories.StoryRepository + Chapters() repositories.ChapterRepository + Comments() repositories.CommentRepository Close(ctx context.Context) error } diff --git a/database/mongodb/changes.go b/database/mongodb/changes.go index 3875552..59de15c 100644 --- a/database/mongodb/changes.go +++ b/database/mongodb/changes.go @@ -33,6 +33,8 @@ func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) } if len(filter.Keys) > 0 { query["keys"] = bson.M{"$in": filter.Keys} + } else { + query["listed"] = true } if filter.Author != nil && *filter.Author != "" { query["author"] = *filter.Author diff --git a/database/mongodb/chapters.go b/database/mongodb/chapters.go new file mode 100644 index 0000000..51309c1 --- /dev/null +++ b/database/mongodb/chapters.go @@ -0,0 +1,137 @@ +package mongodb + +import ( + "context" + "git.aiterp.net/rpdata/api/internal/generate" + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/repositories" + "github.com/globalsign/mgo" + "github.com/globalsign/mgo/bson" + "log" +) + +type chapterRepository struct { + chapters *mgo.Collection + comments *mgo.Collection +} + +func newChapterRepository(db *mgo.Database) (repositories.ChapterRepository, error) { + collection := db.C("story.chapters") + + err := collection.EnsureIndexKey("storyId") + if err != nil { + return nil, err + } + err = collection.EnsureIndexKey("author") + if err != nil { + return nil, err + } + err = collection.EnsureIndexKey("createdDate") + if err != nil { + return nil, err + } + + return &chapterRepository{ + chapters: collection, + comments: db.C("story.comments"), + }, nil +} + +func (r *chapterRepository) Find(ctx context.Context, id string) (*models.Chapter, error) { + chapter := new(models.Chapter) + err := r.chapters.FindId(id).One(chapter) + if err != nil { + return nil, err + } + + return chapter, nil +} + +func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) { + query := bson.M{} + if filter.StoryID != nil { + query["storyId"] = *filter.StoryID + } + + chapters := make([]*models.Chapter, 0, 32) + err := r.chapters.Find(query).Sort("createdDate").Limit(filter.Limit).All(&chapters) + if err != nil { + if err == mgo.ErrNotFound { + return chapters, nil + } + + return nil, err + } + + return chapters, nil +} + +func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) { + chapter.ID = generate.StoryID() + + err := r.chapters.Insert(chapter) + if err != nil { + return nil, err + } + + return &chapter, nil +} + +func (r *chapterRepository) Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) { + updateBson := bson.M{} + if update.Title != nil { + updateBson["title"] = *update.Title + chapter.Title = *update.Title + } + if update.Source != nil { + updateBson["source"] = *update.Source + chapter.Source = *update.Source + } + if update.FictionalDate != nil { + updateBson["fictionalDate"] = *update.FictionalDate + chapter.FictionalDate = *update.FictionalDate + } + if update.CommentMode != nil { + updateBson["commentMode"] = *update.CommentMode + chapter.CommentMode = *update.CommentMode + } + if update.CommentsLocked != nil { + updateBson["commentsLocked"] = *update.CommentsLocked + chapter.CommentsLocked = *update.CommentsLocked + } + + err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": updateBson}) + if err != nil { + return nil, err + } + + return &chapter, nil +} + +func (r *chapterRepository) Move(ctx context.Context, chapter models.Chapter, from, to models.Story) (*models.Chapter, error) { + err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": bson.M{"storyId": to.ID}}) + if err != nil { + return nil, err + } + + chapter.StoryID = to.ID + + return &chapter, nil +} + +func (r *chapterRepository) Delete(ctx context.Context, chapter models.Chapter) error { + err := r.chapters.RemoveId(chapter.ID) + if err != nil { + return err + } + + c, err := r.comments.RemoveAll(bson.M{"chapterId": chapter.ID}) + if err != nil { + log.Println("Failed to remove comments:", err) + return nil + } + + log.Printf("Removed chapter %s (%d comments)", chapter.ID, c.Removed) + + return nil +} diff --git a/database/mongodb/comments.go b/database/mongodb/comments.go new file mode 100644 index 0000000..9017cdc --- /dev/null +++ b/database/mongodb/comments.go @@ -0,0 +1,134 @@ +package mongodb + +import ( + "context" + "git.aiterp.net/rpdata/api/internal/generate" + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/repositories" + "github.com/globalsign/mgo" + "github.com/globalsign/mgo/bson" + "log" +) + +type commentRepository struct { + comments *mgo.Collection +} + +func newCommentRepository(db *mgo.Database) (repositories.CommentRepository, error) { + collection := db.C("story.comments") + + err := collection.EnsureIndexKey("chapterId") + if err != nil { + return nil, err + } + err = collection.EnsureIndexKey("author") + if err != nil { + return nil, err + } + err = collection.EnsureIndexKey("createdDate") + if err != nil { + return nil, err + } + + r := &commentRepository{ + comments: collection, + } + + go r.fixFieldTypo() + + return r, nil +} + +func (r *commentRepository) Find(ctx context.Context, id string) (*models.Comment, error) { + comment := new(models.Comment) + err := r.comments.FindId(id).One(comment) + if err != nil { + return nil, err + } + + return comment, nil +} + +func (r *commentRepository) List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) { + query := bson.M{} + if filter.ChapterID != nil { + query["chapterId"] = *filter.ChapterID + } + + comments := make([]*models.Comment, 0, 32) + err := r.comments.Find(query).Sort("createdDate").Limit(filter.Limit).All(&comments) + if err != nil { + if err == mgo.ErrNotFound { + return comments, nil + } + + return nil, err + } + + return comments, nil +} + +func (r *commentRepository) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) { + comment.ID = generate.CommentID() + + err := r.comments.Insert(comment) + if err != nil { + return nil, err + } + + return &comment, nil +} + +func (r *commentRepository) Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Comment, error) { + updateBson := bson.M{} + if update.Subject != nil { + updateBson["subject"] = *update.Subject + comment.Subject = *update.Subject + } + if update.Source != nil { + updateBson["source"] = *update.Source + comment.Source = *update.Source + } + if update.FictionalDate != nil { + updateBson["fictionalDate"] = *update.FictionalDate + comment.FictionalDate = *update.FictionalDate + } + if update.CharacterID != nil { + updateBson["characterId"] = *update.CharacterID + comment.CharacterID = *update.CharacterID + } + if update.CharacterName != nil { + updateBson["characterName"] = *update.CharacterName + comment.CharacterName = *update.CharacterName + } + + err := r.comments.UpdateId(comment.ID, bson.M{"$set": updateBson}) + if err != nil { + return nil, err + } + + return &comment, nil +} + +func (r *commentRepository) Delete(ctx context.Context, comment models.Comment) error { + return r.comments.RemoveId(comment.ID) +} + +func (r *commentRepository) fixFieldTypo() { + c, err := r.comments.UpdateAll(bson.M{ + "editeddDate": bson.M{"$ne": nil}, + }, bson.M{ + "$rename": bson.M{"editeddDate": "editedDate"}, + }) + if err != nil { + if err == mgo.ErrNotFound { + return + } + + log.Println("Failed to run name typo fix:", err) + return + } + if c.Updated > 0 { + log.Println("Fixed editeddDate field name typo in", c.Updated, "comments") + } +} diff --git a/database/mongodb/db.go b/database/mongodb/db.go index f949fd0..b2d4e20 100644 --- a/database/mongodb/db.go +++ b/database/mongodb/db.go @@ -21,7 +21,9 @@ type MongoDB struct { logs *logRepository posts *postRepository files *fileRepository - story repositories.StoryRepository + stories repositories.StoryRepository + chapters repositories.ChapterRepository + comments repositories.CommentRepository } func (m *MongoDB) Changes() repositories.ChangeRepository { @@ -52,8 +54,16 @@ func (m *MongoDB) Files() repositories.FileRepository { return m.files } -func (m *MongoDB) Story() repositories.StoryRepository { - return m.story +func (m *MongoDB) Stories() repositories.StoryRepository { + return m.stories +} + +func (m *MongoDB) Chapters() repositories.ChapterRepository { + return m.chapters +} + +func (m *MongoDB) Comments() repositories.CommentRepository { + return m.comments } func (m *MongoDB) Close(ctx context.Context) error { @@ -119,7 +129,19 @@ func Init(cfg config.Database) (*MongoDB, error) { return nil, err } - story, err := newStoryRepository(db) + stories, err := newStoryRepository(db) + if err != nil { + session.Close() + return nil, err + } + + chapters, err := newChapterRepository(db) + if err != nil { + session.Close() + return nil, err + } + + comments, err := newCommentRepository(db) if err != nil { session.Close() return nil, err @@ -134,7 +156,9 @@ func Init(cfg config.Database) (*MongoDB, error) { characters: characters, channels: channels, tags: newTagRepository(db), - story: story, + stories: stories, + chapters: chapters, + comments: comments, logs: logs, posts: posts, files: files, diff --git a/database/mongodb/stories.go b/database/mongodb/stories.go index 9c23069..9de602b 100644 --- a/database/mongodb/stories.go +++ b/database/mongodb/stories.go @@ -72,7 +72,9 @@ func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ( query["tags"] = bson.M{"$all": filter.Tags} } if filter.Unlisted != nil { - query["listed"] = *filter.Unlisted + query["listed"] = !*filter.Unlisted + } else { + query["listed"] = true } if !filter.EarliestFictionalDate.IsZero() && !filter.LatestFictionalDate.IsZero() { query["fictionalDate"] = bson.M{ @@ -90,7 +92,7 @@ func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ( } stories := make([]*models.Story, 0, 32) - err := r.stories.Find(query).Sort("-updatedDate ").Limit(filter.Limit).All(&stories) + err := r.stories.Find(query).Sort("-updatedDate").Limit(filter.Limit).All(&stories) if err != nil { if err == mgo.ErrNotFound { return stories, nil @@ -195,7 +197,7 @@ func (r *storyRepository) Delete(ctx context.Context, story models.Story) error return nil } - c2, err := r.chapters.RemoveAll(bson.M{"comments": bson.M{"$in": chapterIds}}) + c2, err := r.comments.RemoveAll(bson.M{"chapterId": bson.M{"$in": chapterIds}}) if err != nil { log.Println("Failed to remove comments:", err) return nil diff --git a/go.mod b/go.mod index a6d2f80..4898018 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( golang.org/x/net v0.0.0-20190514140710-3ec191127204 // indirect golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/text v0.3.0 // indirect + golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd google.golang.org/appengine v1.1.0 // indirect gopkg.in/ini.v1 v1.42.0 // indirect gopkg.in/yaml.v2 v2.2.2 diff --git a/graph2/complexity.go b/graph2/complexity.go index c2dcc12..163c7f2 100644 --- a/graph2/complexity.go +++ b/graph2/complexity.go @@ -4,7 +4,6 @@ import ( "git.aiterp.net/rpdata/api/graph2/graphcore" "git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/models/files" - "git.aiterp.net/rpdata/api/models/stories" ) func complexity() (cr graphcore.ComplexityRoot) { @@ -40,7 +39,7 @@ func complexity() (cr graphcore.ComplexityRoot) { return childComplexity + findComplexity } cr.Query.Logs = func(childComplexity int, filter *models.LogFilter) int { - if filter != nil && filter.Open != nil && *filter.Open == true { + if filter != nil && ((filter.Open != nil && *filter.Open == true) || (filter.Limit <= 10)) { return childComplexity + findComplexity } @@ -58,7 +57,11 @@ func complexity() (cr graphcore.ComplexityRoot) { cr.Query.Story = func(childComplexity int, id string) int { return childComplexity + findComplexity } - cr.Query.Stories = func(childComplexity int, filter *stories.Filter) int { + cr.Query.Stories = func(childComplexity int, filter *models.StoryFilter) int { + if filter != nil && filter.Limit <= 10 { + return childComplexity + findComplexity + } + return childComplexity + listComplexity } cr.Query.File = func(childComplexity int, id string) int { diff --git a/graph2/gqlgen.yml b/graph2/gqlgen.yml index c7d3d68..cdba4dc 100644 --- a/graph2/gqlgen.yml +++ b/graph2/gqlgen.yml @@ -48,7 +48,7 @@ models: StoryCategory: model: git.aiterp.net/rpdata/api/models.StoryCategory StoriesFilter: - model: git.aiterp.net/rpdata/api/models/stories.Filter + model: git.aiterp.net/rpdata/api/models.StoryFilter File: model: git.aiterp.net/rpdata/api/models.File FilesFilter: diff --git a/graph2/graph.go b/graph2/graph.go index 04eb156..b19786b 100644 --- a/graph2/graph.go +++ b/graph2/graph.go @@ -39,15 +39,15 @@ func (r *rootResolver) Log() graphcore.LogResolver { } func (r *rootResolver) Comment() graphcore.CommentResolver { - return &types.CommentResolver + return types.CommentResolver(r.s) } func (r *rootResolver) Chapter() graphcore.ChapterResolver { - return &types.ChapterResolver + return types.ChapterResolver(r.s) } func (r *rootResolver) Story() graphcore.StoryResolver { - return &types.StoryResolver + return types.StoryResolver(r.s) } func (r *rootResolver) Change() graphcore.ChangeResolver { diff --git a/graph2/resolvers/chapter.go b/graph2/resolvers/chapter.go index 967a8d0..ed8614c 100644 --- a/graph2/resolvers/chapter.go +++ b/graph2/resolvers/chapter.go @@ -2,54 +2,24 @@ package resolvers import ( "context" - "errors" "time" "git.aiterp.net/rpdata/api/graph2/graphcore" - "git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/models" - "git.aiterp.net/rpdata/api/models/changekeys" - "git.aiterp.net/rpdata/api/models/changes" - "git.aiterp.net/rpdata/api/models/chapters" - "git.aiterp.net/rpdata/api/models/comments" - "git.aiterp.net/rpdata/api/models/stories" ) // Queries func (r *queryResolver) Chapter(ctx context.Context, id string) (*models.Chapter, error) { - chapter, err := chapters.FindID(id) - if err != nil { - return nil, err - } - - return &chapter, nil + return r.s.Stories.FindChapter(ctx, id) } // Mutations func (r *mutationResolver) AddChapter(ctx context.Context, input graphcore.ChapterAddInput) (*models.Chapter, error) { - story, err := stories.FindID(input.StoryID) + story, err := r.s.Stories.FindStory(ctx, input.StoryID) if err != nil { - return nil, errors.New("Story not found") - } - - token := auth.TokenFromContext(ctx) - if !token.Permitted("member", "story.add") { - return nil, errors.New("Unauthorized") - } - - author := token.UserID - if input.Author != nil && *input.Author != author { - if !token.Permitted("story.add") { - return nil, errors.New("False pretender") - } - - author = *input.Author - } - - if !story.Open && story.Author != author { - return nil, errors.New("Story is not open") + return nil, err } commentMode := models.ChapterCommentModeDisabled @@ -57,121 +27,45 @@ func (r *mutationResolver) AddChapter(ctx context.Context, input graphcore.Chapt commentMode = *input.CommentMode } - chapter, err := chapters.Add(story, input.Title, author, input.Source, time.Now(), input.FictionalDate, commentMode) - if err != nil { - return nil, errors.New("Failed to create chapter: " + err.Error()) - } - - go changes.Submit("Chapter", "add", token.UserID, story.Listed, changekeys.Listed(story, chapter), story, chapter) - - return &chapter, nil + return r.s.Stories.CreateChapter(ctx, *story, input.Title, input.Source, input.Author, time.Now(), input.FictionalDate, commentMode) } func (r *mutationResolver) MoveChapter(ctx context.Context, input graphcore.ChapterMoveInput) (*models.Chapter, error) { - chapter, err := chapters.FindID(input.ID) + chapter, err := r.s.Stories.FindChapter(ctx, input.ID) if err != nil { - return nil, errors.New("Chapter not found") - } - - token := auth.TokenFromContext(ctx) - if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "chapter.move") { - return nil, errors.New("You are not allowed to move this chapter") + return nil, err } - - target, err := stories.FindID(input.StoryID) + from, err := r.s.Stories.FindStory(ctx, chapter.StoryID) if err != nil { - return nil, errors.New("Target story not found") - } - - if !target.Open && !token.PermittedUser(target.Author, "member", "chapter.move") { - return nil, errors.New("You are not permitted to move chapters to this story") + return nil, err } - - oldStoryID := chapter.StoryID - chapter, err = chapters.Move(chapter, target) + to, err := r.s.Stories.FindStory(ctx, input.StoryID) if err != nil { - return nil, errors.New("Failed to move chapter: " + err.Error()) + return nil, err } - go func() { - story, err := stories.FindID(chapter.StoryID) - if err != nil { - story.ID = chapter.StoryID - } - - oldStory, err := stories.FindID(oldStoryID) - if err != nil { - oldStory.ID = oldStoryID - } - - changes.Submit("Chapter", "move-out", chapter.Author, oldStory.Listed, changekeys.Many(oldStory, chapter), chapter) - changes.Submit("Chapter", "move-in", token.UserID, story.Listed, changekeys.Listed(story, chapter), story, chapter) - }() - - return &chapter, nil + return r.s.Stories.MoveChapter(ctx, chapter, *from, *to) } func (r *mutationResolver) EditChapter(ctx context.Context, input graphcore.ChapterEditInput) (*models.Chapter, error) { - chapter, err := chapters.FindID(input.ID) - if err != nil { - return nil, errors.New("Chapter not found") - } - - token := auth.TokenFromContext(ctx) - if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "chapter.edit") { - return nil, errors.New("Unauthorized") - } - - if input.ClearFictionalDate != nil && *input.ClearFictionalDate == true { - input.FictionalDate = &time.Time{} - } - - chapter, err = chapters.Edit(chapter, input.Title, input.Source, input.FictionalDate, input.CommentMode, input.CommentsLocked) + chapter, err := r.s.Stories.FindChapter(ctx, input.ID) if err != nil { - return nil, errors.New("Failed to edit chapter: " + err.Error()) + return nil, err } - go func() { - story, err := stories.FindID(chapter.StoryID) - if err != nil { - story.ID = chapter.StoryID - } - - changes.Submit("Chapter", "edit", token.UserID, story.Listed, changekeys.Many(story, chapter), chapter) - }() - - return &chapter, nil + return r.s.Stories.EditChapter(ctx, chapter, input.Title, input.Source, input.FictionalDate, input.CommentMode, input.CommentsLocked) } func (r *mutationResolver) RemoveChapter(ctx context.Context, input graphcore.ChapterRemoveInput) (*models.Chapter, error) { - chapter, err := chapters.FindID(input.ID) + chapter, err := r.s.Stories.FindChapter(ctx, input.ID) if err != nil { - return nil, errors.New("Chapter not found") - } - - token := auth.TokenFromContext(ctx) - if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "chapter.remove") { - return nil, errors.New("Unauthorized") - } - - chapter, err = chapters.Remove(chapter) - if err != nil { - return nil, errors.New("Failed to remove chapter: " + err.Error()) + return nil, err } - err = comments.RemoveChapter(chapter) + err = r.s.Stories.RemoveChapter(ctx, chapter) if err != nil { - return nil, errors.New("Chapter was removed, but comment removal failed: " + err.Error()) + return nil, err } - go func() { - story, err := stories.FindID(chapter.StoryID) - if err != nil { - story.ID = chapter.StoryID - } - - changes.Submit("Chapter", "remove", token.UserID, story.Listed, changekeys.Many(story, chapter), chapter) - }() - - return &chapter, nil + return chapter, nil } diff --git a/graph2/resolvers/comment.go b/graph2/resolvers/comment.go index 284f287..dfffd77 100644 --- a/graph2/resolvers/comment.go +++ b/graph2/resolvers/comment.go @@ -2,64 +2,24 @@ package resolvers import ( "context" - "errors" - "log" "time" "git.aiterp.net/rpdata/api/graph2/graphcore" - "git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/models" - "git.aiterp.net/rpdata/api/models/changekeys" - "git.aiterp.net/rpdata/api/models/changes" - "git.aiterp.net/rpdata/api/models/chapters" - "git.aiterp.net/rpdata/api/models/characters" - "git.aiterp.net/rpdata/api/models/comments" - "git.aiterp.net/rpdata/api/models/stories" ) // Queries func (r *queryResolver) Comment(ctx context.Context, id string) (*models.Comment, error) { - comment, err := comments.Find(id) - if err != nil { - return nil, err - } - - return &comment, nil + return r.s.Stories.FindComment(ctx, id) } // Mutations func (r *mutationResolver) AddComment(ctx context.Context, input graphcore.CommentAddInput) (*models.Comment, error) { - chapter, err := chapters.FindID(input.ChapterID) + chapter, err := r.s.Stories.FindChapter(ctx, input.ChapterID) if err != nil { - return nil, errors.New("Chapter not found") - } - - token := auth.TokenFromContext(ctx) - if !token.Permitted("member", "story.edit") { - return nil, errors.New("Unauthorized") - } - - if !chapter.CanComment() { - return nil, errors.New("Comments are disabled or locked") - } - - var characterPtr *models.Character - if input.CharacterID != nil { - character, err := characters.FindID(*input.CharacterID) - if err != nil { - return nil, errors.New("Character not found") - } else if character.Author != token.UserID { - return nil, errors.New("That is not your character") - } - - characterPtr = &character - } - - fictionalDate := time.Time{} - if input.FictionalDate != nil { - fictionalDate = *input.FictionalDate + return nil, err } subject := "" @@ -67,109 +27,38 @@ func (r *mutationResolver) AddComment(ctx context.Context, input graphcore.Comme subject = *input.Subject } - comment, err := comments.Add(chapter, subject, token.UserID, input.Source, input.CharacterName, characterPtr, time.Now(), fictionalDate) - if err != nil { - return nil, errors.New("Failed to add comment: " + err.Error()) + fictionalDate := time.Time{} + if input.FictionalDate != nil { + fictionalDate = *input.FictionalDate } - go func() { - story, err := stories.FindID(chapter.StoryID) - if err != nil { - log.Println("WARNING: Couldn't log comment change:", err) - return - } - - changes.Submit("Comment", "add", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story) - }() - - return &comment, nil + return r.s.Stories.CreateComment(ctx, *chapter, subject, "", input.Source, input.CharacterName, input.CharacterID, time.Now(), fictionalDate) } func (r *mutationResolver) EditComment(ctx context.Context, input graphcore.CommentEditInput) (*models.Comment, error) { - comment, err := comments.Find(input.CommentID) + comment, err := r.s.Stories.FindComment(ctx, input.CommentID) if err != nil { - return nil, errors.New("Comment not found") - } - - token := auth.TokenFromContext(ctx) - if !token.PermittedUser(comment.Author, "member", "story.edit") { - return nil, errors.New("You cannot edit this comment") - } - - chapter, err := chapters.FindID(comment.ChapterID) - if err != nil { - return nil, errors.New("Comment's chapter not found") - } - - if !chapter.CanComment() { - return nil, errors.New("Comments are disabled or locked") - } - - if input.ClearFictionalDate != nil && *input.ClearFictionalDate == true { - input.FictionalDate = &time.Time{} - } - - if input.CharacterID != nil && *input.CharacterID != "" { - character, err := characters.FindID(*input.CharacterID) - if err != nil { - return nil, errors.New("Character not found") - } else if character.Author != token.UserID { - return nil, errors.New("That is not your character") - } + return nil, err } - comment, err = comments.Edit(comment, input.Source, input.CharacterName, input.CharacterID, input.Subject, input.FictionalDate) - if err != nil { - return nil, errors.New("Could not post comment: " + err.Error()) + fictionalDate := input.FictionalDate + if input.ClearFictionalDate != nil && *input.ClearFictionalDate { + fictionalDate = &time.Time{} } - go func() { - story, err := stories.FindID(chapter.StoryID) - if err != nil { - log.Println("WARNING: Couldn't log comment change:", err) - return - } - - changes.Submit("Comment", "edit", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story) - }() - - return &comment, nil + return r.s.Stories.EditComment(ctx, comment, input.Source, input.CharacterName, input.CharacterID, input.Subject, fictionalDate) } func (r *mutationResolver) RemoveComment(ctx context.Context, input graphcore.CommentRemoveInput) (*models.Comment, error) { - comment, err := comments.Find(input.CommentID) - if err != nil { - return nil, errors.New("Comment not found") - } - - token := auth.TokenFromContext(ctx) - if !token.PermittedUser(comment.Author, "member", "story.edit") { - return nil, errors.New("You cannot remove this comment") - } - - chapter, err := chapters.FindID(comment.ChapterID) + comment, err := r.s.Stories.FindComment(ctx, input.CommentID) if err != nil { - return nil, errors.New("Comment's chapter not found") - } - - if !chapter.CanComment() { - return nil, errors.New("Comments are disabled or locked") + return nil, err } - err = comments.Remove(comment) + err = r.s.Stories.RemoveComment(ctx, comment) if err != nil { - return nil, errors.New("Failed to remove comment: " + err.Error()) + return nil, err } - go func() { - story, err := stories.FindID(chapter.StoryID) - if err != nil { - log.Println("WARNING: Couldn't log comment change:", err) - return - } - - changes.Submit("Comment", "remove", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story) - }() - - return &comment, nil + return comment, nil } diff --git a/graph2/resolvers/story.go b/graph2/resolvers/story.go index 52da184..1ce03f9 100644 --- a/graph2/resolvers/story.go +++ b/graph2/resolvers/story.go @@ -2,199 +2,80 @@ package resolvers import ( "context" - "errors" "time" "git.aiterp.net/rpdata/api/graph2/graphcore" - "git.aiterp.net/rpdata/api/internal/auth" "git.aiterp.net/rpdata/api/models" - "git.aiterp.net/rpdata/api/models/changekeys" - "git.aiterp.net/rpdata/api/models/changes" - "git.aiterp.net/rpdata/api/models/chapters" - "git.aiterp.net/rpdata/api/models/stories" ) func (r *queryResolver) Story(ctx context.Context, id string) (*models.Story, error) { - story, err := stories.FindID(id) - if err != nil { - return nil, err - } - - return &story, nil + return r.s.Stories.FindStory(ctx, id) } -func (r *queryResolver) Stories(ctx context.Context, filter *stories.Filter) ([]*models.Story, error) { - if filter != nil { - if filter.Unlisted != nil && *filter.Unlisted == true { - token := auth.TokenFromContext(ctx) - if !token.Authenticated() { - return nil, errors.New("You are not permitted to view unlisted stories") - } - - if !token.Permitted("story.unlisted") { - filter.Author = &token.UserID - } - } - } - - stories, err := stories.List(filter) - if err != nil { - return nil, err +func (r *queryResolver) Stories(ctx context.Context, filter *models.StoryFilter) ([]*models.Story, error) { + if filter == nil { + filter = &models.StoryFilter{} } - stories2 := make([]*models.Story, len(stories)) - for i := range stories { - stories2[i] = &stories[i] - } - - return stories2, nil + return r.s.Stories.ListStories(ctx, *filter) } // Mutations func (r *mutationResolver) AddStory(ctx context.Context, input graphcore.StoryAddInput) (*models.Story, error) { - token := auth.TokenFromContext(ctx) - if token == nil || !token.Permitted("member", "story.add") { - return nil, errors.New("Permission denied") - } - - author := token.UserID - if input.Author != nil && *input.Author != author { - if !token.Permitted("story.add") { - return nil, errors.New("You are not permitted to add a story in another author's name") - } - - author = *input.Author - } - - fictionalDate := time.Time{} - if input.FictionalDate != nil { - fictionalDate = *input.FictionalDate - } - listed := input.Listed != nil && *input.Listed open := input.Open != nil && *input.Open tags := make([]models.Tag, len(input.Tags)) - for i := range input.Tags { - tags[i] = *input.Tags[i] + for i, tag := range input.Tags { + tags[i] = *tag } - story, err := stories.Add(input.Name, author, input.Category, listed, open, tags, time.Now(), fictionalDate) - if err != nil { - return nil, errors.New("Failed to add story: " + err.Error()) + fictionalDate := time.Time{} + if input.FictionalDate != nil { + fictionalDate = *input.FictionalDate } - go changes.Submit("Story", "add", token.UserID, story.Listed, changekeys.Listed(story), story) - - return &story, nil + return r.s.Stories.CreateStory(ctx, input.Name, input.Author, input.Category, listed, open, tags, time.Now(), fictionalDate) } func (r *mutationResolver) AddStoryTag(ctx context.Context, input graphcore.StoryTagAddInput) (*models.Story, error) { - token := auth.TokenFromContext(ctx) - - story, err := stories.FindID(input.ID) + story, err := r.s.Stories.FindStory(ctx, input.ID) if err != nil { - return nil, errors.New("Story not found") - } - - if story.Open { - if !token.Permitted("member") { - return nil, errors.New("You are not permitted to edit this story") - } - } else { - if !token.PermittedUser(story.Author, "member", "story.edit") { - return nil, errors.New("You are not permitted to edit this story") - } - } - - story, err = stories.AddTag(story, *input.Tag) - if err != nil { - return nil, errors.New("Failed to add story: " + err.Error()) + return nil, err } - go changes.Submit("Story", "tag", token.UserID, story.Listed, changekeys.Listed(story), story, input.Tag) - - return &story, nil + return r.s.Stories.AddStoryTag(ctx, *story, *input.Tag) } func (r *mutationResolver) RemoveStoryTag(ctx context.Context, input graphcore.StoryTagRemoveInput) (*models.Story, error) { - token := auth.TokenFromContext(ctx) - - story, err := stories.FindID(input.ID) + story, err := r.s.Stories.FindStory(ctx, input.ID) if err != nil { - return nil, errors.New("Story not found") - } - - if story.Open { - if !token.Permitted("member") { - return nil, errors.New("You are not permitted to edit this story") - } - } else { - if !token.PermittedUser(story.Author, "member", "story.edit") { - return nil, errors.New("You are not permitted to edit this story") - } - } - - story, err = stories.RemoveTag(story, *input.Tag) - if err != nil { - return nil, errors.New("Failed to add story: " + err.Error()) + return nil, err } - go changes.Submit("Story", "untag", token.UserID, story.Listed, changekeys.Listed(story), story, input.Tag) - - return &story, nil + return r.s.Stories.RemoveStoryTag(ctx, *story, *input.Tag) } func (r *mutationResolver) EditStory(ctx context.Context, input graphcore.StoryEditInput) (*models.Story, error) { - token := auth.TokenFromContext(ctx) - - story, err := stories.FindID(input.ID) + story, err := r.s.Stories.FindStory(ctx, input.ID) if err != nil { - return nil, errors.New("Story not found") - } - - if !token.PermittedUser(story.Author, "member", "story.edit") { - return nil, errors.New("You are not permitted to remove this story") - } - - if input.ClearFictionalDate != nil && *input.ClearFictionalDate { - input.FictionalDate = &time.Time{} - } - - story, err = stories.Edit(story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate) - if err != nil { - return nil, errors.New("Failed to add story: " + err.Error()) + return nil, err } - go changes.Submit("Story", "edit", token.UserID, story.Listed, changekeys.Listed(story), story) - - return &story, nil + return r.s.Stories.EditStory(ctx, story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate) } func (r *mutationResolver) RemoveStory(ctx context.Context, input graphcore.StoryRemoveInput) (*models.Story, error) { - token := auth.TokenFromContext(ctx) - - story, err := stories.FindID(input.ID) - if err != nil { - return nil, errors.New("Story not found") - } - - if !token.PermittedUser(story.Author, "member", "story.remove") { - return nil, errors.New("You are not permitted to remove this story") - } - - story, err = stories.Remove(story) + story, err := r.s.Stories.FindStory(ctx, input.ID) if err != nil { return nil, err } - err = chapters.RemoveStory(story) + err = r.s.Stories.RemoveStory(ctx, story) if err != nil { - return nil, errors.New("Failed to remove chapters, but story is removed: " + err.Error()) + return nil, err } - go changes.Submit("Story", "remove", token.UserID, story.Listed, changekeys.Listed(story), story) - - return &story, nil + return story, err } diff --git a/graph2/types/chapter.go b/graph2/types/chapter.go index 314c255..772be8d 100644 --- a/graph2/types/chapter.go +++ b/graph2/types/chapter.go @@ -2,14 +2,15 @@ package types import ( "context" - "errors" + "git.aiterp.net/rpdata/api/services" "time" "git.aiterp.net/rpdata/api/models" - "git.aiterp.net/rpdata/api/models/comments" ) -type chapterResolver struct{} +type chapterResolver struct { + stories *services.StoryService +} func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Chapter) (*time.Time, error) { if chapter.FictionalDate.IsZero() { @@ -20,31 +21,15 @@ func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Cha } func (r *chapterResolver) Comments(ctx context.Context, chapter *models.Chapter, limit *int) ([]*models.Comment, error) { - limitValue := 0 - if limit != nil { - if *limit < 0 { - return nil, errors.New("Limit cannot be negative") - } - - limitValue = *limit - } - - if !chapter.CommentMode.IsEnabled() { - return nil, nil + if limit == nil { + limit = new(int) + *limit = 0 } - comments, err := comments.ListChapterID(chapter.ID, limitValue) - if err != nil { - return nil, err - } - - comments2 := make([]*models.Comment, len(comments)) - for i := range comments { - comments2[i] = &comments[i] - } - - return comments2, nil + return r.stories.ListComments(ctx, *chapter, *limit) } // ChapterResolver is a resolver -var ChapterResolver chapterResolver +func ChapterResolver(s *services.Bundle) *chapterResolver { + return &chapterResolver{stories: s.Stories} +} diff --git a/graph2/types/comment.go b/graph2/types/comment.go index a7a9d0b..77d39c7 100644 --- a/graph2/types/comment.go +++ b/graph2/types/comment.go @@ -2,31 +2,33 @@ package types import ( "context" - "errors" + "git.aiterp.net/rpdata/api/repositories" + "git.aiterp.net/rpdata/api/services" + "github.com/globalsign/mgo" "time" - "git.aiterp.net/rpdata/api/internal/loader" "git.aiterp.net/rpdata/api/models" ) -type commentResolver struct{} +type commentResolver struct { + characters *services.CharacterService +} func (r *commentResolver) Character(ctx context.Context, obj *models.Comment) (*models.Character, error) { if obj.CharacterID == "" { return nil, nil } - loader := loader.FromContext(ctx) - if loader == nil { - return nil, errors.New("no loader") - } - - character, err := loader.Character("id", obj.CharacterID) + character, err := r.characters.Find(ctx, obj.CharacterID) if err != nil { + if err == repositories.ErrNotFound || err == mgo.ErrNotFound { + return nil, nil + } + return nil, err } - return &character, nil + return character, nil } func (r *commentResolver) FictionalDate(ctx context.Context, obj *models.Comment) (*time.Time, error) { @@ -38,4 +40,6 @@ func (r *commentResolver) FictionalDate(ctx context.Context, obj *models.Comment } // CommentResolver is a resolver -var CommentResolver commentResolver +func CommentResolver(s *services.Bundle) *commentResolver { + return &commentResolver{characters: s.Characters} +} diff --git a/graph2/types/story.go b/graph2/types/story.go index 1b2e3da..7136ac2 100644 --- a/graph2/types/story.go +++ b/graph2/types/story.go @@ -2,14 +2,15 @@ package types import ( "context" + "git.aiterp.net/rpdata/api/services" "time" - "git.aiterp.net/rpdata/api/models/chapters" - "git.aiterp.net/rpdata/api/models" ) -type storyResolver struct{} +type storyResolver struct { + stories *services.StoryService +} func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) { if story.FictionalDate.IsZero() { @@ -20,18 +21,10 @@ func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) } func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]*models.Chapter, error) { - chapters, err := chapters.ListStoryID(story.ID) - if err != nil { - return nil, err - } - - chapters2 := make([]*models.Chapter, len(chapters)) - for i := range chapters { - chapters2[i] = &chapters[i] - } - - return chapters2, nil + return r.stories.ListChapters(ctx, *story) } // StoryResolver is a resolver -var StoryResolver storyResolver +func StoryResolver(s *services.Bundle) *storyResolver { + return &storyResolver{stories: s.Stories} +} diff --git a/internal/auth/permitted.go b/internal/auth/permitted.go index 8e2270a..5cbd2f7 100644 --- a/internal/auth/permitted.go +++ b/internal/auth/permitted.go @@ -15,8 +15,12 @@ func CheckPermission(ctx context.Context, op string, obj interface{}) error { return ErrUnauthenticated } - if reflect.TypeOf(obj).Kind() != reflect.Ptr { - return CheckPermission(ctx, op, &obj) + if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct { + ptr := reflect.PtrTo(v.Type()) + ptrValue := reflect.New(ptr.Elem()) + ptrValue.Elem().Set(v) + + obj = ptrValue.Interface() } var authorized = false diff --git a/models/change.go b/models/change.go index 8b5699e..17eccbc 100644 --- a/models/change.go +++ b/models/change.go @@ -27,8 +27,12 @@ type Change struct { // AddObject adds the model into the appropriate array. func (change *Change) AddObject(object interface{}) bool { - if v := reflect.ValueOf(object); v.Kind() != reflect.Ptr && v.Kind() != reflect.Slice { - return change.AddObject(v.Addr().Interface()) + if v := reflect.ValueOf(object); v.Kind() == reflect.Struct { + ptr := reflect.PtrTo(v.Type()) + ptrValue := reflect.New(ptr.Elem()) + ptrValue.Elem().Set(v) + + object = ptrValue.Interface() } switch object := object.(type) { diff --git a/repositories/chapter.go b/repositories/chapter.go index f10f254..2267356 100644 --- a/repositories/chapter.go +++ b/repositories/chapter.go @@ -9,6 +9,7 @@ type ChapterRepository interface { Find(ctx context.Context, id string) (*models.Chapter, error) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) - Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Story, error) + Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) + Move(ctx context.Context, chapter models.Chapter, from, to models.Story) (*models.Chapter, error) Delete(ctx context.Context, chapter models.Chapter) error } diff --git a/repositories/comment.go b/repositories/comment.go index bc4aa26..9bd3190 100644 --- a/repositories/comment.go +++ b/repositories/comment.go @@ -9,6 +9,6 @@ type CommentRepository interface { Find(ctx context.Context, id string) (*models.Comment, error) List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) - Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Story, error) + Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Comment, error) Delete(ctx context.Context, comment models.Comment) error } diff --git a/services/services.go b/services/services.go index b03af15..3a22335 100644 --- a/services/services.go +++ b/services/services.go @@ -12,6 +12,7 @@ type Bundle struct { Changes *ChangeService Logs *LogService Channels *ChannelService + Stories *StoryService } // NewBundle creates a new bundle. @@ -38,6 +39,13 @@ func NewBundle(db database.Database) *Bundle { channelService: bundle.Channels, characterService: bundle.Characters, } + bundle.Stories = &StoryService{ + stories: db.Stories(), + chapters: db.Chapters(), + comments: db.Comments(), + changeService: bundle.Changes, + characterService: bundle.Characters, + } return bundle } diff --git a/services/stories.go b/services/stories.go index 405d9aa..cabd7b9 100644 --- a/services/stories.go +++ b/services/stories.go @@ -13,16 +13,25 @@ import ( // StoryService is a service governing all operations on stories and child objects. type StoryService struct { - stories repositories.StoryRepository - chapters repositories.ChapterRepository - comments repositories.CommentRepository - changeService *ChangeService + stories repositories.StoryRepository + chapters repositories.ChapterRepository + comments repositories.CommentRepository + changeService *ChangeService + characterService *CharacterService } func (s *StoryService) FindStory(ctx context.Context, id string) (*models.Story, error) { return s.stories.Find(ctx, id) } +func (s *StoryService) FindChapter(ctx context.Context, id string) (*models.Chapter, error) { + return s.chapters.Find(ctx, id) +} + +func (s *StoryService) FindComment(ctx context.Context, id string) (*models.Comment, error) { + return s.comments.Find(ctx, id) +} + func (s *StoryService) ListStories(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) { return s.stories.List(ctx, filter) } @@ -35,10 +44,19 @@ func (s *StoryService) ListComments(ctx context.Context, chapter models.Chapter, return s.comments.List(ctx, models.CommentFilter{ChapterID: &chapter.ID, Limit: limit}) } -func (s *StoryService) CreateStory(ctx context.Context, name, 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) (*models.Story, error) { + if author == nil { + token := auth.TokenFromContext(ctx) + if token == nil { + return nil, auth.ErrUnauthenticated + } + + author = &token.UserID + } + story := &models.Story{ Name: name, - Author: author, + Author: *author, Category: category, Listed: listed, Open: open, @@ -62,12 +80,21 @@ func (s *StoryService) CreateStory(ctx context.Context, name, author string, cat return story, nil } -func (s *StoryService) CreateChapter(ctx context.Context, story models.Story, title, author, source string, createdDate time.Time, fictionalDate *time.Time, commentMode models.ChapterCommentMode) (*models.Chapter, error) { +func (s *StoryService) CreateChapter(ctx context.Context, story models.Story, title, source string, author *string, createdDate time.Time, fictionalDate *time.Time, commentMode models.ChapterCommentMode) (*models.Chapter, error) { + if author == nil { + token := auth.TokenFromContext(ctx) + if token == nil { + return nil, auth.ErrUnauthenticated + } + + author = &token.UserID + } + chapter := &models.Chapter{ ID: generate.ChapterID(), StoryID: story.ID, Title: title, - Author: author, + Author: *author, Source: source, CreatedDate: createdDate, EditedDate: createdDate, @@ -102,11 +129,13 @@ func (s *StoryService) CreateChapter(ctx context.Context, story models.Story, ti return chapter, nil } -// CreateComment adds a comment. -func (s *StoryService) CreateComment(ctx context.Context, chapter models.Chapter, subject, author, source, characterName string, character *models.Character, createdDate time.Time, fictionalDate time.Time) (*models.Comment, error) { - characterID := "" - if character != nil { - characterID = character.ID +func (s *StoryService) CreateComment(ctx context.Context, chapter models.Chapter, subject, author, source, characterName string, characterID *string, createdDate time.Time, fictionalDate time.Time) (*models.Comment, error) { + if characterID != nil { + if err := s.permittedCharacter(ctx, "comment", *characterID); err != nil { + return nil, err + } + } else { + characterID = new(string) } if !chapter.CanComment() { @@ -127,7 +156,7 @@ func (s *StoryService) CreateComment(ctx context.Context, chapter models.Chapter Subject: subject, Author: author, CharacterName: characterName, - CharacterID: characterID, + CharacterID: *characterID, FictionalDate: fictionalDate, CreatedDate: createdDate, EditedDate: createdDate, @@ -145,8 +174,245 @@ func (s *StoryService) CreateComment(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, comment), comment) } else { - s.changeService.Submit(ctx, "Comment", "add", false, changekeys.Many(chapter, comment), comment) + s.changeService.Submit(ctx, "Comment", "add", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter, comment), comment) + } + + 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) { + if story == nil { + panic("StoryService.Edit called with nil story") + } + + if err := auth.CheckPermission(ctx, "edit", story); err != nil { + return nil, err + } + + story, err := s.stories.Update(ctx, *story, models.StoryUpdate{ + Name: name, + Open: open, + Listed: listed, + Category: category, + FictionalDate: fictionalDate, + }) + if err != nil { + return nil, err + } + + s.changeService.Submit(ctx, "Story", "edit", story.Listed, changekeys.Listed(story), story) + + return story, nil +} + +func (s *StoryService) AddStoryTag(ctx context.Context, story models.Story, tag models.Tag) (*models.Story, error) { + if err := auth.CheckPermission(ctx, "edit", &story); err != nil { + return nil, err + } + + err := s.stories.AddTag(ctx, story, tag) + if err != nil { + return nil, err + } + + story.Tags = append(story.Tags, tag) + + s.changeService.Submit(ctx, "Story", "tag", story.Listed, changekeys.Listed(story), story, tag) + + return &story, nil +} + +func (s *StoryService) RemoveStoryTag(ctx context.Context, story models.Story, tag models.Tag) (*models.Story, error) { + if err := auth.CheckPermission(ctx, "edit", &story); err != nil { + return nil, err + } + + err := s.stories.RemoveTag(ctx, story, tag) + if err != nil { + return nil, err + } + + for i, tag2 := range story.Tags { + if tag2 == tag { + story.Tags = append(story.Tags[:i], story.Tags[i+1:]...) + break + } + } + + s.changeService.Submit(ctx, "Story", "untag", story.Listed, changekeys.Listed(story), story, tag) + + return &story, nil +} + +func (s *StoryService) EditChapter(ctx context.Context, chapter *models.Chapter, title, source *string, fictionalDate *time.Time, commentMode *models.ChapterCommentMode, commentsLocked *bool) (*models.Chapter, error) { + if chapter == nil { + panic("StoryService.EditChapter called with nil chapter") + } + + if err := auth.CheckPermission(ctx, "edit", chapter); err != nil { + return nil, err + } + + chapter, err := s.chapters.Update(ctx, *chapter, models.ChapterUpdate{ + Title: title, + Source: source, + FictionalDate: fictionalDate, + CommentMode: commentMode, + CommentsLocked: commentsLocked, + }) + if err != nil { + return nil, err + } + + if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil { + s.changeService.Submit(ctx, "Comment", "add", story.Listed, changekeys.Many(story, chapter), chapter) + } else { + s.changeService.Submit(ctx, "Comment", "add", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter), chapter) + } + + return chapter, nil +} + +func (s *StoryService) MoveChapter(ctx context.Context, chapter *models.Chapter, from, to models.Story) (*models.Chapter, error) { + if err := auth.CheckPermission(ctx, "move", chapter); err != nil { + return nil, err + } + + if to.Open { + if !auth.TokenFromContext(ctx).Permitted("member", "chapter.add") { + return nil, auth.ErrUnauthorized + } + } else { + if err := auth.CheckPermission(ctx, "add", chapter); err != nil { + return nil, err + } + } + + chapter, err := s.chapters.Move(ctx, *chapter, from, to) + if err != nil { + return nil, err + } + + s.changeService.Submit(ctx, "Chapter", "move-out", from.Listed, changekeys.Listed(from), chapter) + s.changeService.Submit(ctx, "Chapter", "move-in", to.Listed, changekeys.Listed(to), chapter) + + return chapter, nil +} + +func (s *StoryService) EditComment(ctx context.Context, comment *models.Comment, source, characterName, characterID, subject *string, fictionalDate *time.Time) (*models.Comment, error) { + if comment == nil { + panic("StoryService.EditChapter called with nil chapter") + } + + if err := auth.CheckPermission(ctx, "edit", comment); err != nil { + return nil, err + } + + if characterID != nil && *characterID != "" && *characterID != comment.CharacterID { + if err := s.permittedCharacter(ctx, "comment", *characterID); err != nil { + return nil, err + } + } + + chapter, err := s.chapters.Find(ctx, comment.ChapterID) + if err != nil { + return nil, errors.New("could not find chapter") + } + if !chapter.CanComment() { + return nil, errors.New("comments are locked or disabled") + } + + comment, err = s.comments.Update(ctx, *comment, models.CommentUpdate{ + Source: source, + CharacterName: characterName, + CharacterID: characterID, + FictionalDate: fictionalDate, + Subject: subject, + }) + if err != nil { + return nil, err + } + + if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil { + s.changeService.Submit(ctx, "Comment", "edit", story.Listed, changekeys.Many(story, chapter, comment), comment) + } else { + s.changeService.Submit(ctx, "Comment", "edit", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter, comment), comment) } return comment, nil } + +func (s *StoryService) RemoveStory(ctx context.Context, story *models.Story) error { + if err := auth.CheckPermission(ctx, "add", story); err != nil { + return err + } + + err := s.stories.Delete(ctx, *story) + if err != nil { + return err + } + + s.changeService.Submit(ctx, "Story", "remove", story.Listed, changekeys.Listed(story), story) + + return nil +} + +func (s *StoryService) RemoveChapter(ctx context.Context, chapter *models.Chapter) error { + if err := auth.CheckPermission(ctx, "remove", chapter); err != nil { + return err + } + + err := s.chapters.Delete(ctx, *chapter) + if err != nil { + return err + } + + if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil { + s.changeService.Submit(ctx, "Chapter", "remove", story.Listed, changekeys.Many(story, chapter), chapter) + } else { + s.changeService.Submit(ctx, "Chapter", "remove", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter), chapter) + } + + return nil +} + +func (s *StoryService) RemoveComment(ctx context.Context, comment *models.Comment) error { + if err := auth.CheckPermission(ctx, "remove", comment); err != nil { + return err + } + + chapter, err := s.chapters.Find(ctx, comment.ChapterID) + if err != nil { + return errors.New("could not find parent chapter") + } + if !chapter.CanComment() { + return errors.New("comments are locked or disabled") + } + + err = s.comments.Delete(ctx, *comment) + if err != nil { + return err + } + + if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil { + s.changeService.Submit(ctx, "Chapter", "remove", story.Listed, changekeys.Many(story, chapter, comment), comment) + } else { + s.changeService.Submit(ctx, "Chapter", "remove", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter, comment), comment) + } + + return nil +} + +func (s *StoryService) permittedCharacter(ctx context.Context, permissionKind, characterID string) error { + character, err := s.characterService.Find(ctx, characterID) + if err != nil { + return errors.New("character could not be found") + } + + token := auth.TokenFromContext(ctx) + if character.Author != token.UserID && !token.Permitted(permissionKind+".edit") { + return errors.New("you are not permitted to use others' character") + } + + return nil +}