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

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