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.

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