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.

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