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.

396 lines
8.8 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. ChannelName 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. ChannelName: 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. limit := 0
  73. if filter != nil {
  74. // Run a text search
  75. if filter.Search != nil {
  76. searchResults := make([]string, 0, 32)
  77. postMutex.RLock()
  78. err := postCollection.Find(bson.M{"$text": bson.M{"$search": *filter.Search}}).Distinct("logId", &searchResults)
  79. if err != nil {
  80. return nil, err
  81. }
  82. postMutex.RUnlock()
  83. // Posts always use shortId to refer to the log
  84. query["shortId"] = bson.M{"$in": searchResults}
  85. }
  86. // Find logs including any of the specified events and channels
  87. if filter.Channels != nil {
  88. query["channel"] = bson.M{"$in": *filter.Channels}
  89. }
  90. if filter.Events != nil {
  91. query["event"] = bson.M{"$in": *filter.Events}
  92. }
  93. // Find logs including all of the specified character IDs.
  94. if filter.Characters != nil {
  95. query["characterIds"] = bson.M{"$all": *filter.Characters}
  96. }
  97. // Limit to only open logs
  98. if filter.Open != nil {
  99. query["open"] = *filter.Open
  100. }
  101. // Set the limit from the filter
  102. limit = filter.Limit
  103. }
  104. return listLog(query, limit)
  105. }
  106. // Edit sets the metadata
  107. func (log *Log) Edit(title *string, event *string, description *string, open *bool) error {
  108. changes := bson.M{}
  109. if title != nil && *title != log.Title {
  110. changes["title"] = *title
  111. }
  112. if event != nil && *event != log.Event {
  113. changes["event"] = *event
  114. }
  115. if description != nil && *description != log.Description {
  116. changes["description"] = *description
  117. }
  118. if open != nil && *open != log.Open {
  119. changes["open"] = *open
  120. }
  121. if len(changes) == 0 {
  122. return nil
  123. }
  124. err := logsCollection.UpdateId(log.ID, bson.M{"$set": changes})
  125. if err != nil {
  126. return err
  127. }
  128. if title != nil {
  129. log.Title = *title
  130. }
  131. if event != nil {
  132. log.Event = *event
  133. }
  134. if description != nil {
  135. log.Description = *description
  136. }
  137. if open != nil {
  138. log.Open = *open
  139. }
  140. return nil
  141. }
  142. // Characters get all the characters for the character IDs stored in the
  143. // log file.
  144. func (log *Log) Characters() ([]character.Character, error) {
  145. return character.ListIDs(log.CharacterIDs...)
  146. }
  147. // Channel gets the channel.
  148. func (log *Log) Channel() (channel.Channel, error) {
  149. return channel.FindName(log.ChannelName)
  150. }
  151. // Posts gets all the posts under the log. If no kinds are specified, it
  152. // will get all posts
  153. func (log *Log) Posts(kinds []string) ([]Post, error) {
  154. postMutex.RLock()
  155. defer postMutex.RUnlock()
  156. query := bson.M{
  157. "$or": []bson.M{
  158. bson.M{"logId": log.ID},
  159. bson.M{"logId": log.ShortID},
  160. },
  161. }
  162. if len(kinds) > 0 {
  163. for i := range kinds {
  164. kinds[i] = strings.ToLower(kinds[i])
  165. }
  166. query["kind"] = bson.M{"$in": kinds}
  167. }
  168. posts, err := listPosts(query)
  169. if err != nil {
  170. return nil, err
  171. }
  172. sort.SliceStable(posts, func(i, j int) bool {
  173. return posts[i].Position < posts[j].Position
  174. })
  175. return posts, nil
  176. }
  177. // NewPost creates a new post.
  178. func (log *Log) NewPost(time time.Time, kind, nick, text string) (Post, error) {
  179. if kind == "" || nick == "" || text == "" {
  180. return Post{}, errors.New("Missing/empty parameters")
  181. }
  182. postMutex.RLock()
  183. defer postMutex.RUnlock()
  184. position, err := counter.Next("next_post_id", log.ShortID)
  185. if err != nil {
  186. return Post{}, err
  187. }
  188. post := Post{
  189. ID: MakePostID(time),
  190. Position: position,
  191. LogID: log.ShortID,
  192. Time: time,
  193. Kind: kind,
  194. Nick: nick,
  195. Text: text,
  196. }
  197. err = postCollection.Insert(post)
  198. if err != nil {
  199. return Post{}, err
  200. }
  201. return post, nil
  202. }
  203. // UpdateCharacters updates the character list
  204. func (log *Log) UpdateCharacters() error {
  205. characterUpdateMutex.Lock()
  206. defer characterUpdateMutex.Unlock()
  207. posts, err := log.Posts([]string{"action", "text", "chars"})
  208. if err != nil {
  209. return err
  210. }
  211. added := make(map[string]bool)
  212. removed := make(map[string]bool)
  213. for _, post := range posts {
  214. if post.Kind == "text" || post.Kind == "action" {
  215. if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") {
  216. continue
  217. }
  218. // Clean up the nick (remove possessive suffix, comma, formatting stuff)
  219. if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
  220. post.Nick = post.Nick[:len(post.Nick)-2]
  221. } else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
  222. post.Nick = post.Nick[:len(post.Nick)-1]
  223. }
  224. added[post.Nick] = true
  225. }
  226. if post.Kind == "chars" {
  227. tokens := strings.Fields(post.Text)
  228. for _, token := range tokens {
  229. if strings.HasPrefix(token, "-") {
  230. removed[token[1:]] = true
  231. } else {
  232. added[strings.Replace(token, "+", "", 1)] = true
  233. }
  234. }
  235. }
  236. }
  237. nicks := make([]string, 0, len(added))
  238. for nick := range added {
  239. if added[nick] && !removed[nick] {
  240. nicks = append(nicks, nick)
  241. }
  242. }
  243. characters, err := character.ListNicks(nicks...)
  244. if err != nil {
  245. return err
  246. }
  247. characterIDs := make([]string, len(characters))
  248. for i, char := range characters {
  249. characterIDs[i] = char.ID
  250. }
  251. err = logsCollection.UpdateId(log.ID, bson.M{"$set": bson.M{"characterIds": characterIDs}})
  252. if err != nil {
  253. return err
  254. }
  255. for _, nick := range nicks {
  256. found := false
  257. for _, character := range characters {
  258. if character.HasNick(nick) {
  259. found = true
  260. break
  261. }
  262. }
  263. if !found {
  264. addUnknownNick(nick)
  265. }
  266. }
  267. log.CharacterIDs = characterIDs
  268. return nil
  269. }
  270. // Remove removes the log and all associated posts from the database
  271. func (log *Log) Remove() error {
  272. err := logsCollection.Remove(bson.M{"_id": log.ID})
  273. if err != nil {
  274. return err
  275. }
  276. _, err = postCollection.RemoveAll(bson.M{"$or": []bson.M{
  277. bson.M{"logId": log.ID},
  278. bson.M{"logId": log.ShortID},
  279. }})
  280. if err != nil {
  281. return err
  282. }
  283. return nil
  284. }
  285. func findLog(query interface{}) (Log, error) {
  286. log := Log{}
  287. err := logsCollection.Find(query).One(&log)
  288. if err != nil {
  289. return Log{}, err
  290. }
  291. return log, nil
  292. }
  293. func listLog(query interface{}, limit int) ([]Log, error) {
  294. logs := make([]Log, 0, 64)
  295. err := logsCollection.Find(query).Limit(limit).Sort("-date").All(&logs)
  296. if err != nil {
  297. return nil, err
  298. }
  299. return logs, nil
  300. }
  301. func iterLogs(query interface{}, limit int) *mgo.Iter {
  302. return logsCollection.Find(query).Sort("-date").Limit(limit).Batch(8).Iter()
  303. }
  304. // MakeLogID generates log IDs that are of the format from logbot2, though it will break compatibility.
  305. func MakeLogID(date time.Time, channel string) string {
  306. return fmt.Sprintf("%s%03d_%s", date.UTC().Format("2006-01-02_150405"), (date.Nanosecond() / int(time.Millisecond/time.Nanosecond)), channel[1:])
  307. }
  308. func init() {
  309. store.HandleInit(func(db *mgo.Database) {
  310. logsCollection = db.C("logbot3.logs")
  311. logsCollection.EnsureIndexKey("date")
  312. logsCollection.EnsureIndexKey("channel")
  313. logsCollection.EnsureIndexKey("characterIds")
  314. logsCollection.EnsureIndexKey("event")
  315. logsCollection.EnsureIndex(mgo.Index{
  316. Key: []string{"channel", "open"},
  317. })
  318. err := logsCollection.EnsureIndex(mgo.Index{
  319. Key: []string{"shortId"},
  320. Unique: true,
  321. DropDups: true,
  322. })
  323. if err != nil {
  324. log.Fatalln("init logbot3.logs:", err)
  325. }
  326. })
  327. }