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.

384 lines
8.4 KiB

  1. package log
  2. import (
  3. "errors"
  4. "fmt"
  5. "log"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "sync"
  10. "time"
  11. "git.aiterp.net/rpdata/api/internal/store"
  12. "git.aiterp.net/rpdata/api/model/channel"
  13. "git.aiterp.net/rpdata/api/model/character"
  14. "git.aiterp.net/rpdata/api/model/counter"
  15. "github.com/globalsign/mgo/bson"
  16. "github.com/globalsign/mgo"
  17. )
  18. var postMutex sync.RWMutex
  19. var characterUpdateMutex sync.Mutex
  20. var logsCollection *mgo.Collection
  21. // Log is the header/session for a log file.
  22. type Log struct {
  23. ID string `bson:"_id"`
  24. ShortID string `bson:"shortId"`
  25. Date time.Time `bson:"date"`
  26. Channel string `bson:"channel"`
  27. Title string `bson:"title,omitempty"`
  28. Event string `bson:"event,omitempty"`
  29. Description string `bson:"description,omitempty"`
  30. Open bool `bson:"open"`
  31. CharacterIDs []string `bson:"characterIds"`
  32. }
  33. // New creates a new Log
  34. func New(date time.Time, channelName, title, event, description string, open bool) (Log, error) {
  35. nextID, err := counter.Next("auto_increment", "Log")
  36. if err != nil {
  37. return Log{}, err
  38. }
  39. _, err = channel.Ensure(channelName, open)
  40. if err != nil {
  41. return Log{}, err
  42. }
  43. log := Log{
  44. ID: MakeLogID(date, channelName),
  45. ShortID: "L" + strconv.Itoa(nextID),
  46. Date: date,
  47. Channel: channelName,
  48. Title: title,
  49. Event: event,
  50. Description: description,
  51. Open: open,
  52. CharacterIDs: nil,
  53. }
  54. err = logsCollection.Insert(log)
  55. if err != nil {
  56. return Log{}, err
  57. }
  58. return log, nil
  59. }
  60. // FindID finds a log either by it's ID or short ID.
  61. func FindID(id string) (Log, error) {
  62. return findLog(bson.M{
  63. "$or": []bson.M{
  64. bson.M{"_id": id},
  65. bson.M{"shortId": id},
  66. },
  67. })
  68. }
  69. // List lists all logs
  70. func List(filter *Filter) ([]Log, error) {
  71. query := bson.M{}
  72. // Run a text search
  73. if filter.Search != nil {
  74. searchResults := make([]string, 0, 32)
  75. postMutex.RLock()
  76. err := postCollection.Find(bson.M{"$text": bson.M{"$search": *filter.Search}}).Distinct("logId", &searchResults)
  77. if err != nil {
  78. return nil, err
  79. }
  80. postMutex.RUnlock()
  81. // Posts always use shortId to refer to the log
  82. query["shortId"] = bson.M{"$in": searchResults}
  83. }
  84. // Find logs including any of the specified events and channels
  85. if filter.Channels != nil {
  86. query["channel"] = bson.M{"$in": *filter.Channels}
  87. }
  88. if filter.Events != nil {
  89. query["event"] = bson.M{"$in": *filter.Events}
  90. }
  91. // Find logs including all of the specified character IDs.
  92. if filter.Characters != nil {
  93. query["characterIds"] = bson.M{"$all": *filter.Characters}
  94. }
  95. // Limit to only open logs
  96. if filter.Open != nil {
  97. query["open"] = *filter.Open
  98. }
  99. limit := 0
  100. if filter.Limit != nil {
  101. limit = int(*filter.Limit)
  102. }
  103. return listLog(query, limit)
  104. }
  105. // Edit sets the metadata
  106. func (log *Log) Edit(title *string, event *string, description *string, open *bool) error {
  107. changes := bson.M{}
  108. if title != nil && *title != log.Title {
  109. changes["title"] = *title
  110. }
  111. if event != nil && *event != log.Event {
  112. changes["event"] = *event
  113. }
  114. if description != nil && *description != log.Description {
  115. changes["description"] = *description
  116. }
  117. if open != nil && *open != log.Open {
  118. changes["open"] = *open
  119. }
  120. if len(changes) == 0 {
  121. return nil
  122. }
  123. err := logsCollection.UpdateId(log.ID, bson.M{"$set": changes})
  124. if err != nil {
  125. return err
  126. }
  127. if title != nil {
  128. log.Title = *title
  129. }
  130. if event != nil {
  131. log.Event = *event
  132. }
  133. if description != nil {
  134. log.Description = *description
  135. }
  136. if open != nil {
  137. log.Open = *open
  138. }
  139. return nil
  140. }
  141. // Posts gets all the posts under the log. If no kinds are specified, it
  142. // will get all posts
  143. func (log *Log) Posts(kinds ...string) ([]Post, error) {
  144. postMutex.RLock()
  145. defer postMutex.RUnlock()
  146. query := bson.M{
  147. "$or": []bson.M{
  148. bson.M{"logId": log.ID},
  149. bson.M{"logId": log.ShortID},
  150. },
  151. }
  152. if len(kinds) > 0 {
  153. for i := range kinds {
  154. kinds[i] = strings.ToLower(kinds[i])
  155. }
  156. query["kind"] = bson.M{"$in": kinds}
  157. }
  158. posts, err := listPosts(query)
  159. if err != nil {
  160. return nil, err
  161. }
  162. sort.SliceStable(posts, func(i, j int) bool {
  163. return posts[i].Position < posts[j].Position
  164. })
  165. return posts, nil
  166. }
  167. // NewPost creates a new post.
  168. func (log *Log) NewPost(time time.Time, kind, nick, text string) (Post, error) {
  169. if kind == "" || nick == "" || text == "" {
  170. return Post{}, errors.New("Missing/empty parameters")
  171. }
  172. postMutex.RLock()
  173. defer postMutex.RUnlock()
  174. position, err := counter.Next("next_post_id", log.ShortID)
  175. if err != nil {
  176. return Post{}, err
  177. }
  178. post := Post{
  179. ID: MakePostID(time),
  180. Position: position,
  181. LogID: log.ShortID,
  182. Time: time,
  183. Kind: kind,
  184. Nick: nick,
  185. Text: text,
  186. }
  187. err = postCollection.Insert(post)
  188. if err != nil {
  189. return Post{}, err
  190. }
  191. return post, nil
  192. }
  193. // UpdateCharacters updates the character list
  194. func (log *Log) UpdateCharacters() error {
  195. characterUpdateMutex.Lock()
  196. defer characterUpdateMutex.Unlock()
  197. posts, err := log.Posts()
  198. if err != nil {
  199. return err
  200. }
  201. added := make(map[string]bool)
  202. removed := make(map[string]bool)
  203. for _, post := range posts {
  204. if post.Kind == "text" || post.Kind == "action" {
  205. if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") {
  206. continue
  207. }
  208. // Clean up the nick (remove possessive suffix, comma, formatting stuff)
  209. if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
  210. post.Nick = post.Nick[:len(post.Nick)-2]
  211. } else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
  212. post.Nick = post.Nick[:len(post.Nick)-1]
  213. }
  214. added[post.Nick] = true
  215. }
  216. if post.Kind == "chars" {
  217. tokens := strings.Fields(post.Text)
  218. for _, token := range tokens {
  219. if strings.HasPrefix(token, "-") {
  220. removed[token[1:]] = true
  221. } else {
  222. added[strings.Replace(token, "+", "", 1)] = true
  223. }
  224. }
  225. }
  226. }
  227. nicks := make([]string, 0, len(added))
  228. for nick := range added {
  229. if added[nick] && !removed[nick] {
  230. nicks = append(nicks, nick)
  231. }
  232. }
  233. characters, err := character.ListNicks(nicks...)
  234. if err != nil {
  235. return err
  236. }
  237. characterIDs := make([]string, len(characters))
  238. for i, char := range characters {
  239. characterIDs[i] = char.ID
  240. }
  241. err = logsCollection.UpdateId(log.ID, bson.M{"$set": bson.M{"characterIds": characterIDs}})
  242. if err != nil {
  243. return err
  244. }
  245. for _, nick := range nicks {
  246. found := false
  247. for _, character := range characters {
  248. if character.HasNick(nick) {
  249. found = true
  250. break
  251. }
  252. }
  253. if !found {
  254. addUnknownNick(nick)
  255. }
  256. }
  257. log.CharacterIDs = characterIDs
  258. return nil
  259. }
  260. // Remove removes the log and all associated posts from the database
  261. func (log *Log) Remove() error {
  262. err := logsCollection.Remove(bson.M{"_id": log.ID})
  263. if err != nil {
  264. return err
  265. }
  266. _, err = postCollection.RemoveAll(bson.M{"$or": []bson.M{
  267. bson.M{"logId": log.ID},
  268. bson.M{"logId": log.ShortID},
  269. }})
  270. if err != nil {
  271. return err
  272. }
  273. return nil
  274. }
  275. func findLog(query interface{}) (Log, error) {
  276. log := Log{}
  277. err := logsCollection.Find(query).One(&log)
  278. if err != nil {
  279. return Log{}, err
  280. }
  281. return log, nil
  282. }
  283. func listLog(query interface{}, limit int) ([]Log, error) {
  284. logs := make([]Log, 0, 64)
  285. err := logsCollection.Find(query).Limit(limit).Sort("-date").All(&logs)
  286. if err != nil {
  287. return nil, err
  288. }
  289. return logs, nil
  290. }
  291. func iterLogs(query interface{}, limit int) *mgo.Iter {
  292. return logsCollection.Find(query).Sort("-date").Limit(limit).Batch(8).Iter()
  293. }
  294. // MakeLogID generates log IDs that are of the format from logbot2, though it will break compatibility.
  295. func MakeLogID(date time.Time, channel string) string {
  296. return fmt.Sprintf("%s%03d_%s", date.UTC().Format("2006-01-02_150405"), (date.Nanosecond() / int(time.Millisecond/time.Nanosecond)), channel[1:])
  297. }
  298. func init() {
  299. store.HandleInit(func(db *mgo.Database) {
  300. logsCollection = db.C("logbot3.logs")
  301. logsCollection.EnsureIndexKey("date")
  302. logsCollection.EnsureIndexKey("channel")
  303. logsCollection.EnsureIndexKey("characterIds")
  304. logsCollection.EnsureIndexKey("event")
  305. logsCollection.EnsureIndex(mgo.Index{
  306. Key: []string{"channel", "open"},
  307. })
  308. err := logsCollection.EnsureIndex(mgo.Index{
  309. Key: []string{"shortId"},
  310. Unique: true,
  311. DropDups: true,
  312. })
  313. if err != nil {
  314. log.Fatalln("init logbot3.logs:", err)
  315. }
  316. })
  317. }