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.

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