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.

444 lines
13 KiB

  1. package services
  2. import (
  3. "context"
  4. "errors"
  5. "git.aiterp.net/rpdata/api/internal/generate"
  6. "git.aiterp.net/rpdata/api/models"
  7. "git.aiterp.net/rpdata/api/models/changekeys"
  8. "git.aiterp.net/rpdata/api/repositories"
  9. "sort"
  10. "time"
  11. )
  12. // StoryService is a service governing all operations on stories and child objects.
  13. type StoryService struct {
  14. stories repositories.StoryRepository
  15. chapters repositories.ChapterRepository
  16. comments repositories.CommentRepository
  17. changeService *ChangeService
  18. characterService *CharacterService
  19. authService *AuthService
  20. }
  21. func (s *StoryService) FindStory(ctx context.Context, id string) (*models.Story, error) {
  22. return s.stories.Find(ctx, id)
  23. }
  24. func (s *StoryService) FindChapter(ctx context.Context, id string) (*models.Chapter, error) {
  25. return s.chapters.Find(ctx, id)
  26. }
  27. func (s *StoryService) FindComment(ctx context.Context, id string) (*models.Comment, error) {
  28. return s.comments.Find(ctx, id)
  29. }
  30. func (s *StoryService) ListStories(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) {
  31. return s.stories.List(ctx, filter)
  32. }
  33. func (s *StoryService) ListChapters(ctx context.Context, story models.Story) ([]*models.Chapter, error) {
  34. chapters, err := s.chapters.List(ctx, models.ChapterFilter{StoryID: &story.ID, Limit: 0})
  35. if err != nil {
  36. return nil, err
  37. }
  38. if story.SortByFictionalDate {
  39. sort.Slice(chapters, func(i, j int) bool {
  40. if !chapters[i].FictionalDate.IsZero() && !chapters[j].FictionalDate.IsZero() {
  41. if chapters[i].FictionalDate.Equal(chapters[j].FictionalDate) {
  42. return chapters[i].CreatedDate.Before(chapters[j].CreatedDate)
  43. }
  44. return chapters[i].FictionalDate.Before(chapters[j].FictionalDate)
  45. } else if chapters[i].FictionalDate.IsZero() && !chapters[j].FictionalDate.IsZero() {
  46. return false
  47. } else if !chapters[i].FictionalDate.IsZero() && chapters[j].FictionalDate.IsZero() {
  48. return true
  49. } else {
  50. return chapters[i].CreatedDate.Before(chapters[j].CreatedDate)
  51. }
  52. })
  53. }
  54. return chapters, nil
  55. }
  56. func (s *StoryService) ListComments(ctx context.Context, chapter models.Chapter, limit int) ([]*models.Comment, error) {
  57. return s.comments.List(ctx, models.CommentFilter{ChapterID: &chapter.ID, Limit: limit})
  58. }
  59. 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) {
  60. if author == nil {
  61. token := s.authService.TokenFromContext(ctx)
  62. if token == nil {
  63. return nil, ErrUnauthenticated
  64. }
  65. author = &token.UserID
  66. }
  67. story := &models.Story{
  68. Name: name,
  69. Author: *author,
  70. Category: category,
  71. Listed: listed,
  72. Open: open,
  73. Tags: tags,
  74. CreatedDate: createdDate,
  75. FictionalDate: fictionalDate,
  76. UpdatedDate: createdDate,
  77. SortByFictionalDate: sortByFictionalDate,
  78. }
  79. if err := s.authService.CheckPermission(ctx, "add", story); err != nil {
  80. return nil, err
  81. }
  82. story, err := s.stories.Insert(ctx, *story)
  83. if err != nil {
  84. return nil, err
  85. }
  86. s.changeService.Submit(ctx, "Story", "add", story.Listed, changekeys.Listed(story), story)
  87. return story, nil
  88. }
  89. 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) {
  90. if author == nil {
  91. token := s.authService.TokenFromContext(ctx)
  92. if token == nil {
  93. return nil, ErrUnauthenticated
  94. }
  95. author = &token.UserID
  96. }
  97. chapter := &models.Chapter{
  98. ID: generate.ChapterID(),
  99. StoryID: story.ID,
  100. Title: title,
  101. Author: *author,
  102. Source: source,
  103. CreatedDate: createdDate,
  104. EditedDate: createdDate,
  105. CommentMode: commentMode,
  106. CommentsLocked: false,
  107. }
  108. if fictionalDate != nil {
  109. chapter.FictionalDate = *fictionalDate
  110. }
  111. if story.Open {
  112. if !s.authService.TokenFromContext(ctx).Permitted("member", "chapter.add") {
  113. return nil, ErrUnauthorized
  114. }
  115. } else {
  116. if err := s.authService.CheckPermission(ctx, "add", chapter); err != nil {
  117. return nil, err
  118. }
  119. }
  120. chapter, err := s.chapters.Insert(ctx, *chapter)
  121. if err != nil {
  122. return nil, err
  123. }
  124. if createdDate.After(story.UpdatedDate) {
  125. _, _ = s.stories.Update(ctx, story, models.StoryUpdate{UpdatedDate: &createdDate})
  126. }
  127. s.changeService.Submit(ctx, "Chapter", "add", story.Listed, changekeys.Many(story, chapter), chapter)
  128. return chapter, nil
  129. }
  130. 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) {
  131. if characterID != nil {
  132. if err := s.permittedCharacter(ctx, "comment", *characterID); err != nil {
  133. return nil, err
  134. }
  135. } else {
  136. characterID = new(string)
  137. }
  138. if !chapter.CanComment() {
  139. return nil, errors.New("comments are locked or disabled")
  140. }
  141. if author == "" {
  142. if token := s.authService.TokenFromContext(ctx); token != nil {
  143. author = token.UserID
  144. } else {
  145. return nil, ErrUnauthenticated
  146. }
  147. }
  148. comment := &models.Comment{
  149. ID: generate.CommentID(),
  150. ChapterID: chapter.ID,
  151. Subject: subject,
  152. Author: author,
  153. CharacterName: characterName,
  154. CharacterID: *characterID,
  155. FictionalDate: fictionalDate,
  156. CreatedDate: createdDate,
  157. EditedDate: createdDate,
  158. Source: source,
  159. }
  160. if err := s.authService.CheckPermission(ctx, "add", comment); err != nil {
  161. return nil, err
  162. }
  163. comment, err := s.comments.Insert(ctx, *comment)
  164. if err != nil {
  165. return nil, err
  166. }
  167. if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil {
  168. s.changeService.Submit(ctx, "Comment", "add", story.Listed, changekeys.Many(story, chapter, comment), comment)
  169. } else {
  170. s.changeService.Submit(ctx, "Comment", "add", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter, comment), comment)
  171. }
  172. return comment, nil
  173. }
  174. 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) {
  175. if story == nil {
  176. panic("StoryService.Edit called with nil story")
  177. }
  178. if err := s.authService.CheckPermission(ctx, "edit", story); err != nil {
  179. return nil, err
  180. }
  181. story, err := s.stories.Update(ctx, *story, models.StoryUpdate{
  182. Name: name,
  183. Open: open,
  184. Listed: listed,
  185. Category: category,
  186. FictionalDate: fictionalDate,
  187. SortByFictionalDate: sortByFictionalDate,
  188. })
  189. if err != nil {
  190. return nil, err
  191. }
  192. s.changeService.Submit(ctx, "Story", "edit", story.Listed, changekeys.Listed(story), story)
  193. return story, nil
  194. }
  195. func (s *StoryService) AddStoryTag(ctx context.Context, story models.Story, tag models.Tag) (*models.Story, error) {
  196. if err := s.authService.CheckPermission(ctx, "tag", &story); err != nil {
  197. return nil, err
  198. }
  199. err := s.stories.AddTag(ctx, story, tag)
  200. if err != nil {
  201. return nil, err
  202. }
  203. story.Tags = append(story.Tags, tag)
  204. s.changeService.Submit(ctx, "Story", "tag", story.Listed, changekeys.Listed(story), story, tag)
  205. return &story, nil
  206. }
  207. func (s *StoryService) RemoveStoryTag(ctx context.Context, story models.Story, tag models.Tag) (*models.Story, error) {
  208. if err := s.authService.CheckPermission(ctx, "tag", &story); err != nil {
  209. return nil, err
  210. }
  211. err := s.stories.RemoveTag(ctx, story, tag)
  212. if err != nil {
  213. return nil, err
  214. }
  215. for i, tag2 := range story.Tags {
  216. if tag2 == tag {
  217. story.Tags = append(story.Tags[:i], story.Tags[i+1:]...)
  218. break
  219. }
  220. }
  221. s.changeService.Submit(ctx, "Story", "untag", story.Listed, changekeys.Listed(story), story, tag)
  222. return &story, nil
  223. }
  224. func (s *StoryService) EditChapter(ctx context.Context, chapter *models.Chapter, title, source *string, fictionalDate *time.Time, commentMode *models.ChapterCommentMode, commentsLocked *bool) (*models.Chapter, error) {
  225. if chapter == nil {
  226. panic("StoryService.EditChapter called with nil chapter")
  227. }
  228. if err := s.authService.CheckPermission(ctx, "edit", chapter); err != nil {
  229. return nil, err
  230. }
  231. chapter, err := s.chapters.Update(ctx, *chapter, models.ChapterUpdate{
  232. Title: title,
  233. Source: source,
  234. FictionalDate: fictionalDate,
  235. CommentMode: commentMode,
  236. CommentsLocked: commentsLocked,
  237. })
  238. if err != nil {
  239. return nil, err
  240. }
  241. if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil {
  242. s.changeService.Submit(ctx, "Chapter", "edit", story.Listed, changekeys.Many(story, chapter), chapter)
  243. } else {
  244. s.changeService.Submit(ctx, "Chapter", "edit", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter), chapter)
  245. }
  246. return chapter, nil
  247. }
  248. func (s *StoryService) MoveChapter(ctx context.Context, chapter *models.Chapter, from, to models.Story) (*models.Chapter, error) {
  249. if err := s.authService.CheckPermission(ctx, "move", chapter); err != nil {
  250. return nil, err
  251. }
  252. if to.Open {
  253. if !s.authService.TokenFromContext(ctx).Permitted("member", "chapter.add") {
  254. return nil, ErrUnauthorized
  255. }
  256. } else {
  257. if err := s.authService.CheckPermission(ctx, "add", chapter); err != nil {
  258. return nil, err
  259. }
  260. }
  261. chapter, err := s.chapters.Move(ctx, *chapter, from, to)
  262. if err != nil {
  263. return nil, err
  264. }
  265. s.changeService.Submit(ctx, "Chapter", "move-out", from.Listed, changekeys.Listed(from), chapter)
  266. s.changeService.Submit(ctx, "Chapter", "move-in", to.Listed, changekeys.Listed(to), chapter)
  267. return chapter, nil
  268. }
  269. func (s *StoryService) EditComment(ctx context.Context, comment *models.Comment, source, characterName, characterID, subject *string, fictionalDate *time.Time) (*models.Comment, error) {
  270. if comment == nil {
  271. panic("StoryService.EditChapter called with nil chapter")
  272. }
  273. if err := s.authService.CheckPermission(ctx, "edit", comment); err != nil {
  274. return nil, err
  275. }
  276. if characterID != nil && *characterID != "" && *characterID != comment.CharacterID {
  277. if err := s.permittedCharacter(ctx, "comment", *characterID); err != nil {
  278. return nil, err
  279. }
  280. }
  281. chapter, err := s.chapters.Find(ctx, comment.ChapterID)
  282. if err != nil {
  283. return nil, errors.New("could not find chapter")
  284. }
  285. if !chapter.CanComment() {
  286. return nil, errors.New("comments are locked or disabled")
  287. }
  288. comment, err = s.comments.Update(ctx, *comment, models.CommentUpdate{
  289. Source: source,
  290. CharacterName: characterName,
  291. CharacterID: characterID,
  292. FictionalDate: fictionalDate,
  293. Subject: subject,
  294. })
  295. if err != nil {
  296. return nil, err
  297. }
  298. if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil {
  299. s.changeService.Submit(ctx, "Comment", "edit", story.Listed, changekeys.Many(story, chapter, comment), comment)
  300. } else {
  301. s.changeService.Submit(ctx, "Comment", "edit", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter, comment), comment)
  302. }
  303. return comment, nil
  304. }
  305. func (s *StoryService) RemoveStory(ctx context.Context, story *models.Story) error {
  306. if err := s.authService.CheckPermission(ctx, "add", story); err != nil {
  307. return err
  308. }
  309. err := s.stories.Delete(ctx, *story)
  310. if err != nil {
  311. return err
  312. }
  313. s.changeService.Submit(ctx, "Story", "remove", story.Listed, changekeys.Listed(story), story)
  314. return nil
  315. }
  316. func (s *StoryService) RemoveChapter(ctx context.Context, chapter *models.Chapter) error {
  317. if err := s.authService.CheckPermission(ctx, "remove", chapter); err != nil {
  318. return err
  319. }
  320. err := s.chapters.Delete(ctx, *chapter)
  321. if err != nil {
  322. return err
  323. }
  324. if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil {
  325. s.changeService.Submit(ctx, "Chapter", "remove", story.Listed, changekeys.Many(story, chapter), chapter)
  326. } else {
  327. s.changeService.Submit(ctx, "Chapter", "remove", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter), chapter)
  328. }
  329. return nil
  330. }
  331. func (s *StoryService) RemoveComment(ctx context.Context, comment *models.Comment) error {
  332. if err := s.authService.CheckPermission(ctx, "remove", comment); err != nil {
  333. return err
  334. }
  335. chapter, err := s.chapters.Find(ctx, comment.ChapterID)
  336. if err != nil {
  337. return errors.New("could not find parent chapter")
  338. }
  339. if !chapter.CanComment() {
  340. return errors.New("comments are locked or disabled")
  341. }
  342. err = s.comments.Delete(ctx, *comment)
  343. if err != nil {
  344. return err
  345. }
  346. if story, err := s.stories.Find(ctx, chapter.StoryID); err == nil {
  347. s.changeService.Submit(ctx, "Comment", "remove", story.Listed, changekeys.Many(story, chapter, comment), comment)
  348. } else {
  349. s.changeService.Submit(ctx, "Comment", "remove", false, changekeys.Many(models.Story{ID: chapter.StoryID}, chapter, comment), comment)
  350. }
  351. return nil
  352. }
  353. func (s *StoryService) permittedCharacter(ctx context.Context, permissionKind, characterID string) error {
  354. character, err := s.characterService.Find(ctx, characterID)
  355. if err != nil {
  356. return errors.New("character could not be found")
  357. }
  358. token := s.authService.TokenFromContext(ctx)
  359. if character.Author != token.UserID && !token.Permitted(permissionKind+".edit") {
  360. return errors.New("you are not permitted to use others' character")
  361. }
  362. return nil
  363. }