GraphQL API and utilities for the rpdata project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

461 lines
14 KiB

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")
}
}
} else {
unlistedValue := false
filter.Unlisted = &unlistedValue
}
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
}