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.

253 lines
6.2 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 == 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. // AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
  106. // occurs
  107. func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) {
  108. if !story.Open && author != story.Author {
  109. 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)
  110. }
  111. chapter := Chapter{
  112. ID: makeChapterID(),
  113. StoryID: story.ID,
  114. Title: title,
  115. Author: author,
  116. Source: source,
  117. CreatedDate: createdDate,
  118. FictionalDate: finctionalDate,
  119. EditedDate: createdDate,
  120. }
  121. err := chapterCollection.Insert(chapter)
  122. if err != nil {
  123. return Chapter{}, err
  124. }
  125. if createdDate.After(story.UpdatedDate) {
  126. if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil {
  127. story.UpdatedDate = createdDate
  128. }
  129. }
  130. return chapter, nil
  131. }
  132. // New creates a new story.
  133. func New(name, author, category string, listed, open bool, tags []Tag, createdDate, fictionalDate time.Time) (Story, error) {
  134. story := Story{
  135. ID: makeStoryID(),
  136. Name: name,
  137. Author: author,
  138. Category: category,
  139. Listed: listed,
  140. Open: open,
  141. Tags: tags,
  142. CreatedDate: createdDate,
  143. FictionalDate: fictionalDate,
  144. UpdatedDate: createdDate,
  145. }
  146. err := storyCollection.Insert(story)
  147. if err != nil {
  148. return Story{}, err
  149. }
  150. return story, nil
  151. }
  152. // FindID finds a story by ID
  153. func FindID(id string) (Story, error) {
  154. story := Story{}
  155. err := storyCollection.FindId(id).One(&story)
  156. return story, err
  157. }
  158. // List lists stories by any non-zero criteria passed with it.
  159. func List(author string, tags []Tag, earliest, latest time.Time, limit int) ([]Story, error) {
  160. query := bson.M{}
  161. if author != "" {
  162. query["author"] = author
  163. }
  164. if len(tags) > 0 {
  165. query["tags"] = bson.M{"$in": tags}
  166. }
  167. if !earliest.IsZero() && !latest.IsZero() {
  168. query["fictionalDate"] = bson.M{
  169. "$gte": earliest,
  170. "$lt": latest,
  171. }
  172. } else if !latest.IsZero() {
  173. query["fictionalDate"] = bson.M{
  174. "$lt": latest,
  175. }
  176. } else if !earliest.IsZero() {
  177. query["fictionalDate"] = bson.M{
  178. "$gte": earliest,
  179. }
  180. }
  181. stories := make([]Story, 0, 128)
  182. err := storyCollection.Find(query).Limit(limit).One(&stories)
  183. return stories, err
  184. }
  185. // makeStoryID makes a random story ID that's 16 characters long
  186. func makeStoryID() string {
  187. result := "S"
  188. offset := 0
  189. data := make([]byte, 32)
  190. rand.Read(data)
  191. for len(result) < 16 {
  192. result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
  193. offset += 8
  194. if offset >= 32 {
  195. rand.Read(data)
  196. offset = 0
  197. }
  198. }
  199. return result[:16]
  200. }
  201. func init() {
  202. store.HandleInit(func(db *mgo.Database) {
  203. storyCollection = db.C("story.stories")
  204. storyCollection.EnsureIndexKey("tags")
  205. storyCollection.EnsureIndexKey("author")
  206. storyCollection.EnsureIndexKey("updatedDate")
  207. storyCollection.EnsureIndexKey("fictionalDate")
  208. storyCollection.EnsureIndexKey("listed")
  209. })
  210. }