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.

198 lines
4.4 KiB

  1. package log
  2. import (
  3. "crypto/rand"
  4. "encoding/binary"
  5. "errors"
  6. "log"
  7. "strconv"
  8. "time"
  9. "git.aiterp.net/rpdata/api/internal/store"
  10. "github.com/globalsign/mgo"
  11. "github.com/globalsign/mgo/bson"
  12. )
  13. var postCollection *mgo.Collection
  14. // A Post is a part of a log file.
  15. type Post struct {
  16. ID string `bson:"_id"`
  17. LogID string `bson:"logId"`
  18. Time time.Time `bson:"time"`
  19. Kind string `bson:"kind"`
  20. Nick string `bson:"nick"`
  21. Text string `bson:"text"`
  22. Position int `bson:"position"`
  23. }
  24. // Edit the post
  25. func (post *Post) Edit(time *time.Time, kind *string, nick *string, text *string) error {
  26. changes := bson.M{}
  27. changed := false
  28. postCopy := *post
  29. if time != nil && !time.IsZero() && !time.Equal(post.Time) {
  30. changes["time"] = *time
  31. changed = true
  32. postCopy.Time = *time
  33. }
  34. if kind != nil && *kind != "" && *kind != post.Kind {
  35. changes["kind"] = *kind
  36. changed = true
  37. postCopy.Kind = *kind
  38. }
  39. if nick != nil && *nick != "" && *nick != post.Nick {
  40. changes["nick"] = *nick
  41. changed = true
  42. postCopy.Nick = *nick
  43. }
  44. if text != nil && *text != "" && *text != post.Text {
  45. changes["text"] = *text
  46. changed = true
  47. postCopy.Text = *text
  48. }
  49. if !changed {
  50. return nil
  51. }
  52. err := postCollection.UpdateId(post.ID, bson.M{"$set": changes})
  53. if err != nil {
  54. return err
  55. }
  56. *post = postCopy
  57. return nil
  58. }
  59. // Move the post
  60. func (post *Post) Move(toPosition int) error {
  61. if toPosition < 1 {
  62. return errors.New("Invalid position")
  63. }
  64. postMutex.Lock()
  65. defer postMutex.Unlock()
  66. // To avoid problems, only allow target indices that are allowed. If it's 1, then there is bound to
  67. // be a post at the position.
  68. if toPosition > 1 {
  69. existingPost := Post{}
  70. err := postCollection.Find(bson.M{"logId": post.LogID, "position": toPosition}).One(&existingPost)
  71. if err != nil || existingPost.Position != toPosition {
  72. return errors.New("No post found at the position")
  73. }
  74. }
  75. query := bson.M{"logId": post.LogID}
  76. operation := bson.M{"$inc": bson.M{"position": 1}}
  77. if toPosition < post.Position {
  78. query["$and"] = []bson.M{
  79. bson.M{"position": bson.M{"$gte": toPosition}},
  80. bson.M{"position": bson.M{"$lt": post.Position}},
  81. }
  82. } else {
  83. query["$and"] = []bson.M{
  84. bson.M{"position": bson.M{"$gt": post.Position}},
  85. bson.M{"position": bson.M{"$lte": toPosition}},
  86. }
  87. operation["$inc"] = bson.M{"position": -1}
  88. }
  89. _, err := postCollection.UpdateAll(query, operation)
  90. if err != nil {
  91. return errors.New("moving others: " + err.Error())
  92. }
  93. err = postCollection.UpdateId(post.ID, bson.M{"$set": bson.M{"position": toPosition}})
  94. if err != nil {
  95. return errors.New("moving: " + err.Error())
  96. }
  97. post.Position = toPosition
  98. return nil
  99. }
  100. // FindPostID finds a log post by ID.
  101. func FindPostID(id string) (Post, error) {
  102. return findPost(bson.M{"_id": id})
  103. }
  104. // ListPostIDs lists log posts by ID
  105. func ListPostIDs(ids ...string) ([]Post, error) {
  106. return listPosts(bson.M{"_id": bson.M{"$in": ids}})
  107. }
  108. // RemovePost removes a post, moving all subsequent post up one position
  109. func RemovePost(id string) (Post, error) {
  110. postMutex.Lock()
  111. defer postMutex.Unlock()
  112. post, err := findPost(bson.M{"_id": id})
  113. if err != nil {
  114. return Post{}, err
  115. }
  116. err = postCollection.RemoveId(id)
  117. if err != nil {
  118. return Post{}, err
  119. }
  120. _, err = postCollection.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
  121. if err != nil {
  122. return Post{}, err
  123. }
  124. return post, nil
  125. }
  126. func findPost(query interface{}) (Post, error) {
  127. post := Post{}
  128. err := postCollection.Find(query).One(&post)
  129. if err != nil {
  130. return Post{}, err
  131. }
  132. return post, nil
  133. }
  134. func listPosts(query interface{}) ([]Post, error) {
  135. posts := make([]Post, 0, 64)
  136. err := postCollection.Find(query).All(&posts)
  137. if err != nil {
  138. return nil, err
  139. }
  140. return posts, nil
  141. }
  142. // MakePostID makes a random post ID
  143. func MakePostID(time time.Time) string {
  144. data := make([]byte, 4)
  145. rand.Read(data)
  146. return "P" + strconv.FormatInt(time.UnixNano(), 36) + strconv.FormatInt(int64(binary.LittleEndian.Uint32(data)), 36)
  147. }
  148. func init() {
  149. store.HandleInit(func(db *mgo.Database) {
  150. postCollection = db.C("logbot3.posts")
  151. postCollection.EnsureIndexKey("logId")
  152. postCollection.EnsureIndexKey("time")
  153. postCollection.EnsureIndexKey("kind")
  154. postCollection.EnsureIndexKey("position")
  155. err := postCollection.EnsureIndex(mgo.Index{
  156. Key: []string{"$text:text"},
  157. })
  158. if err != nil {
  159. log.Fatalln("init logbot3.logs:", err)
  160. }
  161. })
  162. }