package services import ( "context" "errors" "git.aiterp.net/rpdata/api/internal/generate" "git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/models/changekeys" "git.aiterp.net/rpdata/api/repositories" "sort" "time" ) // 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 characterService *CharacterService authService *AuthService } 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) { if filter.Unlisted != nil && *filter.Unlisted { token := s.authService.TokenFromContext(ctx) if !token.Authenticated() { return nil, errors.New("you cannot view unlisted stories") } if !token.Permitted("story.unlisted") { if filter.Author == nil { filter.Author = &token.UserID } else if *filter.Author != token.UserID { return nil, errors.New("you cannot view your own unlisted stories") } } } return s.stories.List(ctx, filter) } func (s *StoryService) ListChapters(ctx context.Context, story models.Story) ([]*models.Chapter, error) { 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) { 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, sortByFictionalDate bool) (*models.Story, error) { if author == nil { token := s.authService.TokenFromContext(ctx) if token == nil { return nil, ErrUnauthenticated } author = &token.UserID } story := &models.Story{ Name: name, Author: *author, Category: category, Listed: listed, Open: open, Tags: tags, CreatedDate: createdDate, FictionalDate: fictionalDate, UpdatedDate: createdDate, SortByFictionalDate: sortByFictionalDate, } if err := s.authService.CheckPermission(ctx, "add", story); err != nil { return nil, err } story, err := s.stories.Insert(ctx, *story) if err != nil { return nil, err } s.changeService.Submit(ctx, "Story", "add", story.Listed, changekeys.Listed(story), story) return story, nil } 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 := s.authService.TokenFromContext(ctx) if token == nil { return nil, ErrUnauthenticated } author = &token.UserID } chapter := &models.Chapter{ ID: generate.ChapterID(), StoryID: story.ID, Title: title, Author: *author, Source: source, CreatedDate: createdDate, EditedDate: createdDate, CommentMode: commentMode, CommentsLocked: false, } if fictionalDate != nil { chapter.FictionalDate = *fictionalDate } if story.Open { if !s.authService.TokenFromContext(ctx).Permitted("member", "chapter.add") { return nil, ErrUnauthorized } } else { if err := s.authService.CheckPermission(ctx, "add", chapter); err != nil { return nil, err } } chapter, err := s.chapters.Insert(ctx, *chapter) if err != nil { return nil, err } if createdDate.After(story.UpdatedDate) { _, _ = s.stories.Update(ctx, story, models.StoryUpdate{UpdatedDate: &createdDate}) } s.changeService.Submit(ctx, "Chapter", "add", story.Listed, changekeys.Many(story, chapter), chapter) return chapter, nil } 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() { return nil, errors.New("comments are locked or disabled") } if author == "" { if token := s.authService.TokenFromContext(ctx); token != nil { author = token.UserID } else { return nil, ErrUnauthenticated } } comment := &models.Comment{ ID: generate.CommentID(), ChapterID: chapter.ID, Subject: subject, Author: author, CharacterName: characterName, CharacterID: *characterID, FictionalDate: fictionalDate, CreatedDate: createdDate, EditedDate: createdDate, Source: source, } if err := s.authService.CheckPermission(ctx, "add", comment); err != nil { return nil, err } comment, err := s.comments.Insert(ctx, *comment) 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, comment), comment) } else { 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, sortByFictionalDate *bool) (*models.Story, error) { if story == nil { panic("StoryService.Edit called with nil story") } if err := s.authService.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, SortByFictionalDate: sortByFictionalDate, }) 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 := s.authService.CheckPermission(ctx, "tag", &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 := s.authService.CheckPermission(ctx, "tag", &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 := s.authService.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, "Chapter", "edit", story.Listed, changekeys.Many(story, chapter), chapter) } else { s.changeService.Submit(ctx, "Chapter", "edit", 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 := s.authService.CheckPermission(ctx, "move", chapter); err != nil { return nil, err } if to.Open { if !s.authService.TokenFromContext(ctx).Permitted("member", "chapter.add") { return nil, ErrUnauthorized } } else { if err := s.authService.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 := s.authService.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 := s.authService.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 := s.authService.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 := s.authService.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, "Comment", "remove", story.Listed, changekeys.Many(story, chapter, comment), comment) } else { s.changeService.Submit(ctx, "Comment", "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 := s.authService.TokenFromContext(ctx) if character.Author != token.UserID && !token.Permitted(permissionKind+".edit") { return errors.New("you are not permitted to use others' character") } return nil }