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.

275 lines
6.6 KiB

  1. package story
  2. import (
  3. "crypto/rand"
  4. "encoding/binary"
  5. "errors"
  6. "fmt"
  7. "os"
  8. "strconv"
  9. "time"
  10. "git.aiterp.net/rpdata/api/internal/store"
  11. "github.com/globalsign/mgo"
  12. "github.com/globalsign/mgo/bson"
  13. )
  14. var storyCollection *mgo.Collection
  15. // ErrTagAlreadyExists is an error returned by Story.AddTag
  16. var ErrTagAlreadyExists = errors.New("Tag already exists")
  17. // ErrTagNotExists is an error returned by Story.RemoveTag
  18. var ErrTagNotExists = errors.New("Tag does not exist")
  19. // A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
  20. // The story model is a container for multiple chapters this time, in contrast to the previous version.
  21. type Story struct {
  22. ID string `bson:"_id"`
  23. Author string `bson:"author"`
  24. Name string `bson:"name"`
  25. Category string `bson:"category"`
  26. Open bool `bson:"open"`
  27. Listed bool `bson:"listed"`
  28. Tags []Tag `bson:"tags"`
  29. CreatedDate time.Time `bson:"createdDate"`
  30. FictionalDate time.Time `bson:"fictionalDate,omitempty"`
  31. UpdatedDate time.Time `bson:"updatedDate"`
  32. }
  33. // AddTag adds a tag to the story. It returns ErrTagAlreadyExists if the tag is already there
  34. func (story *Story) AddTag(tag Tag) error {
  35. for i := range story.Tags {
  36. if story.Tags[i].Equal(tag) {
  37. return ErrTagAlreadyExists
  38. }
  39. }
  40. err := storyCollection.UpdateId(story.ID, bson.M{"$push": bson.M{"tags": tag}})
  41. if err != nil {
  42. return err
  43. }
  44. story.Tags = append(story.Tags, tag)
  45. return nil
  46. }
  47. // RemoveTag removes a tag to the story. It returns ErrTagNotExists if the tag does not exist.
  48. func (story *Story) RemoveTag(tag Tag) error {
  49. index := -1
  50. for i := range story.Tags {
  51. if story.Tags[i].Equal(tag) {
  52. index = i
  53. break
  54. }
  55. }
  56. if index == -1 {
  57. return ErrTagNotExists
  58. }
  59. err := storyCollection.UpdateId(story.ID, bson.M{"$pull": bson.M{"tags": tag}})
  60. if err != nil {
  61. return err
  62. }
  63. story.Tags = append(story.Tags[:index], story.Tags[index+1:]...)
  64. return nil
  65. }
  66. // Edit edits the story, reflecting the new values in the story's struct values. If nothing will be
  67. // changed, it will silently return without a database roundtrip.
  68. func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDate *time.Time) error {
  69. changes := bson.M{}
  70. changed := *story
  71. if name != nil && *name != story.Name {
  72. changes["name"] = *name
  73. changed.Name = *name
  74. }
  75. if category != nil && *category != story.Category {
  76. changes["category"] = *category
  77. changed.Name = *category
  78. }
  79. if listed != nil && *listed != story.Listed {
  80. changes["listed"] = *listed
  81. changed.Listed = *listed
  82. }
  83. if open != nil && *open != story.Open {
  84. changes["open"] = *open
  85. changed.Open = *open
  86. }
  87. if fictionalDate != nil && !fictionalDate.Equal(story.FictionalDate) {
  88. changes["fictionalDate"] = *fictionalDate
  89. changed.FictionalDate = *fictionalDate
  90. }
  91. if len(changes) == 0 {
  92. return nil
  93. }
  94. err := storyCollection.UpdateId(story.ID, bson.M{"$set": changes})
  95. if err != nil {
  96. return err
  97. }
  98. *story = changed
  99. return nil
  100. }
  101. // Remove the story from the database
  102. func (story *Story) Remove() error {
  103. return storyCollection.RemoveId(story.ID)
  104. }
  105. // Chapters calls ListChapterStoryID with the story's ID:
  106. func (story *Story) Chapters() ([]Chapter, error) {
  107. return ListChapterStoryID(story.ID)
  108. }
  109. // AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
  110. // occurs
  111. func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) {
  112. if !story.Open && author != story.Author {
  113. fmt.Fprintf(os.Stderr, "WARNING: AddChapter is breaking Open rules (story.id=%#+v, story.name=%#+v, chapter.author=%#+v, chapter.title=%#+v)", story.ID, story.Name, author, title)
  114. }
  115. chapter := Chapter{
  116. ID: makeChapterID(),
  117. StoryID: story.ID,
  118. Title: title,
  119. Author: author,
  120. Source: source,
  121. CreatedDate: createdDate,
  122. FictionalDate: finctionalDate,
  123. EditedDate: createdDate,
  124. }
  125. err := chapterCollection.Insert(chapter)
  126. if err != nil {
  127. return Chapter{}, err
  128. }
  129. if createdDate.After(story.UpdatedDate) {
  130. if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil {
  131. story.UpdatedDate = createdDate
  132. }
  133. }
  134. return chapter, nil
  135. }
  136. // New creates a new story.
  137. func New(name, author, category string, listed, open bool, tags []Tag, createdDate, fictionalDate time.Time) (Story, error) {
  138. story := Story{
  139. ID: makeStoryID(),
  140. Name: name,
  141. Author: author,
  142. Category: category,
  143. Listed: listed,
  144. Open: open,
  145. Tags: tags,
  146. CreatedDate: createdDate,
  147. FictionalDate: fictionalDate,
  148. UpdatedDate: createdDate,
  149. }
  150. err := storyCollection.Insert(story)
  151. if err != nil {
  152. return Story{}, err
  153. }
  154. return story, nil
  155. }
  156. // FindID finds a story by ID
  157. func FindID(id string) (Story, error) {
  158. story := Story{}
  159. err := storyCollection.FindId(id).One(&story)
  160. return story, err
  161. }
  162. // List lists stories by any non-zero criteria passed with it.
  163. func List(author string, category string, tags []Tag, earliest, latest time.Time, unlisted bool, open *bool, limit int) ([]Story, error) {
  164. query := bson.M{}
  165. if author != "" {
  166. query["author"] = author
  167. }
  168. if category != "" {
  169. query["category"] = category
  170. }
  171. if len(tags) > 0 {
  172. query["tags"] = bson.M{"$in": tags}
  173. }
  174. if !earliest.IsZero() && !latest.IsZero() {
  175. query["fictionalDate"] = bson.M{
  176. "$gte": earliest,
  177. "$lt": latest,
  178. }
  179. } else if !latest.IsZero() {
  180. query["fictionalDate"] = bson.M{
  181. "$lt": latest,
  182. }
  183. } else if !earliest.IsZero() {
  184. query["fictionalDate"] = bson.M{
  185. "$gte": earliest,
  186. }
  187. }
  188. if unlisted {
  189. query["listed"] = false
  190. }
  191. if open != nil {
  192. query["open"] = *open
  193. }
  194. size := limit
  195. if size == 0 {
  196. size = 128
  197. }
  198. stories := make([]Story, 0, size)
  199. err := storyCollection.Find(query).Limit(limit).Sort("-updatedDate").All(&stories)
  200. return stories, err
  201. }
  202. // makeStoryID makes a random story ID that's 16 characters long
  203. func makeStoryID() string {
  204. result := "S"
  205. offset := 0
  206. data := make([]byte, 32)
  207. rand.Read(data)
  208. for len(result) < 16 {
  209. result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
  210. offset += 8
  211. if offset >= 32 {
  212. rand.Read(data)
  213. offset = 0
  214. }
  215. }
  216. return result[:16]
  217. }
  218. func init() {
  219. store.HandleInit(func(db *mgo.Database) {
  220. storyCollection = db.C("story.stories")
  221. storyCollection.EnsureIndexKey("tags")
  222. storyCollection.EnsureIndexKey("author")
  223. storyCollection.EnsureIndexKey("updatedDate")
  224. storyCollection.EnsureIndexKey("fictionalDate")
  225. storyCollection.EnsureIndexKey("listed")
  226. })
  227. }