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.

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