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.

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