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
380 lines
8.5 KiB
package log
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.aiterp.net/rpdata/api/internal/store"
|
|
"git.aiterp.net/rpdata/api/model/channel"
|
|
"git.aiterp.net/rpdata/api/model/character"
|
|
"git.aiterp.net/rpdata/api/model/counter"
|
|
|
|
"github.com/globalsign/mgo/bson"
|
|
|
|
"github.com/globalsign/mgo"
|
|
)
|
|
|
|
var postMutex sync.RWMutex
|
|
|
|
var characterUpdateMutex sync.Mutex
|
|
|
|
var logsCollection *mgo.Collection
|
|
|
|
// Log is the header/session for a log file.
|
|
type Log struct {
|
|
ID string `bson:"_id"`
|
|
ShortID string `bson:"shortId"`
|
|
Date time.Time `bson:"date"`
|
|
Channel string `bson:"channel"`
|
|
Title string `bson:"title,omitempty"`
|
|
Event string `bson:"event,omitempty"`
|
|
Description string `bson:"description,omitempty"`
|
|
Open bool `bson:"open"`
|
|
CharacterIDs []string `bson:"characterIds"`
|
|
}
|
|
|
|
// New creates a new Log
|
|
func New(date time.Time, channelName, title, event, description string, open bool) (Log, error) {
|
|
nextID, err := counter.Next("auto_increment", "Log")
|
|
if err != nil {
|
|
return Log{}, err
|
|
}
|
|
|
|
_, err = channel.Ensure(channelName, open)
|
|
if err != nil {
|
|
return Log{}, err
|
|
}
|
|
|
|
log := Log{
|
|
ID: MakeLogID(date, channelName),
|
|
ShortID: "L" + strconv.Itoa(nextID),
|
|
Date: date,
|
|
Channel: channelName,
|
|
Title: title,
|
|
Event: event,
|
|
Description: description,
|
|
Open: open,
|
|
CharacterIDs: nil,
|
|
}
|
|
|
|
err = logsCollection.Insert(log)
|
|
if err != nil {
|
|
return Log{}, err
|
|
}
|
|
|
|
return log, nil
|
|
}
|
|
|
|
// FindID finds a log either by it's ID or short ID.
|
|
func FindID(id string) (Log, error) {
|
|
return findLog(bson.M{
|
|
"$or": []bson.M{
|
|
bson.M{"_id": id},
|
|
bson.M{"shortId": id},
|
|
},
|
|
})
|
|
}
|
|
|
|
// List lists all logs
|
|
func List(limit int) ([]Log, error) {
|
|
return listLog(bson.M{}, limit)
|
|
}
|
|
|
|
// Remove removes the log post with this ID. Both the long and short ID is accepted
|
|
func Remove(id string) error {
|
|
return logsCollection.Remove(bson.M{
|
|
"$or": []bson.M{
|
|
bson.M{"_id": id},
|
|
bson.M{"shortId": id},
|
|
},
|
|
})
|
|
}
|
|
|
|
// ListSearch lists the logs matching the parameters. Empty/zero values means the parameter is ingored when
|
|
// building the query. This is the old aitelogs2 way, but with the addition of a text search.
|
|
//
|
|
// If a text search is specified, it will make two trips to the database.
|
|
func ListSearch(textSearch string, channels []string, characterIds []string, events []string, open bool, limit int) ([]Log, error) {
|
|
postMutex.RLock()
|
|
defer postMutex.RUnlock()
|
|
|
|
query := bson.M{}
|
|
|
|
// Run a text search
|
|
if textSearch != "" {
|
|
searchResults := make([]string, 0, 32)
|
|
|
|
err := postCollection.Find(bson.M{"$text": bson.M{"$search": textSearch}}).Distinct("logId", &searchResults)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Posts always use shortId to refer to the log
|
|
query["shortId"] = bson.M{"$in": searchResults}
|
|
}
|
|
|
|
// Find logs including any of the specified events and channels
|
|
if len(channels) > 0 {
|
|
query["channel"] = bson.M{"$in": channels}
|
|
}
|
|
if len(events) > 0 {
|
|
query["events"] = bson.M{"$in": channels}
|
|
}
|
|
|
|
// Find logs including all of the specified character IDs.
|
|
if len(characterIds) > 0 {
|
|
query["characterIds"] = bson.M{"$all": characterIds}
|
|
}
|
|
|
|
// Limit to only open logs
|
|
if open {
|
|
query["open"] = true
|
|
}
|
|
|
|
return listLog(query, limit)
|
|
}
|
|
|
|
// Edit sets the metadata
|
|
func (log *Log) Edit(title *string, event *string, description *string, open *bool) error {
|
|
changes := bson.M{}
|
|
|
|
if title != nil && *title != log.Title {
|
|
changes["title"] = *title
|
|
}
|
|
if event != nil && *event != log.Event {
|
|
changes["event"] = *event
|
|
}
|
|
if description != nil && *description != log.Description {
|
|
changes["description"] = *description
|
|
}
|
|
if open != nil && *open != log.Open {
|
|
changes["open"] = *open
|
|
}
|
|
|
|
if len(changes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
err := logsCollection.UpdateId(log.ID, bson.M{"$set": changes})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if title != nil {
|
|
log.Title = *title
|
|
}
|
|
if event != nil {
|
|
log.Event = *event
|
|
}
|
|
if description != nil {
|
|
log.Description = *description
|
|
}
|
|
if open != nil {
|
|
log.Open = *open
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Posts gets all the posts under the log. If no kinds are specified, it
|
|
// will get all posts
|
|
func (log *Log) Posts(kinds ...string) ([]Post, error) {
|
|
postMutex.RLock()
|
|
defer postMutex.RUnlock()
|
|
|
|
query := bson.M{
|
|
"$or": []bson.M{
|
|
bson.M{"logId": log.ID},
|
|
bson.M{"logId": log.ShortID},
|
|
},
|
|
}
|
|
|
|
if len(kinds) > 0 {
|
|
for i := range kinds {
|
|
kinds[i] = strings.ToLower(kinds[i])
|
|
}
|
|
|
|
query["kind"] = bson.M{"$in": kinds}
|
|
}
|
|
|
|
posts, err := listPosts(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sort.SliceStable(posts, func(i, j int) bool {
|
|
return posts[i].Position < posts[j].Position
|
|
})
|
|
|
|
return posts, nil
|
|
}
|
|
|
|
// NewPost creates a new post.
|
|
func (log *Log) NewPost(time time.Time, kind, nick, text string) (Post, error) {
|
|
if kind == "" || nick == "" || text == "" {
|
|
return Post{}, errors.New("Missing/empty parameters")
|
|
}
|
|
|
|
postMutex.RLock()
|
|
defer postMutex.RUnlock()
|
|
|
|
position, err := counter.Next("next_post_id", log.ShortID)
|
|
if err != nil {
|
|
return Post{}, err
|
|
}
|
|
|
|
post := Post{
|
|
ID: MakePostID(time),
|
|
Position: position,
|
|
LogID: log.ShortID,
|
|
Time: time,
|
|
Kind: kind,
|
|
Nick: nick,
|
|
Text: text,
|
|
}
|
|
|
|
err = postCollection.Insert(post)
|
|
if err != nil {
|
|
return Post{}, err
|
|
}
|
|
|
|
return post, nil
|
|
}
|
|
|
|
// UpdateCharacters updates the character list
|
|
func (log *Log) UpdateCharacters() error {
|
|
characterUpdateMutex.Lock()
|
|
defer characterUpdateMutex.Unlock()
|
|
|
|
posts, err := log.Posts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
added := make(map[string]bool)
|
|
removed := make(map[string]bool)
|
|
for _, post := range posts {
|
|
if post.Kind == "text" || post.Kind == "action" {
|
|
if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") {
|
|
continue
|
|
}
|
|
|
|
// Clean up the nick (remove possessive suffix, comma, formatting stuff)
|
|
if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
|
|
post.Nick = post.Nick[:len(post.Nick)-2]
|
|
} else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
|
|
post.Nick = post.Nick[:len(post.Nick)-1]
|
|
}
|
|
|
|
added[post.Nick] = true
|
|
}
|
|
if post.Kind == "chars" {
|
|
tokens := strings.Fields(post.Text)
|
|
for _, token := range tokens {
|
|
if strings.HasPrefix(token, "-") {
|
|
removed[token[1:]] = true
|
|
} else {
|
|
added[strings.Replace(token, "+", "", 1)] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
nicks := make([]string, 0, len(added))
|
|
for nick := range added {
|
|
if added[nick] && !removed[nick] {
|
|
nicks = append(nicks, nick)
|
|
}
|
|
}
|
|
|
|
characters, err := character.ListNicks(nicks...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
characterIDs := make([]string, len(characters))
|
|
for i, char := range characters {
|
|
characterIDs[i] = char.ID
|
|
}
|
|
|
|
err = logsCollection.UpdateId(log.ID, bson.M{"$set": bson.M{"characterIds": characterIDs}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, nick := range nicks {
|
|
found := false
|
|
|
|
for _, character := range characters {
|
|
if character.HasNick(nick) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
addUnknownNick(nick)
|
|
}
|
|
}
|
|
|
|
log.CharacterIDs = characterIDs
|
|
|
|
return nil
|
|
}
|
|
|
|
func findLog(query interface{}) (Log, error) {
|
|
log := Log{}
|
|
err := logsCollection.Find(query).One(&log)
|
|
if err != nil {
|
|
return Log{}, err
|
|
}
|
|
|
|
return log, nil
|
|
}
|
|
|
|
func listLog(query interface{}, limit int) ([]Log, error) {
|
|
logs := make([]Log, 0, 64)
|
|
err := logsCollection.Find(query).Limit(limit).Sort("-date").All(&logs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return logs, nil
|
|
}
|
|
|
|
func iterLogs(query interface{}, limit int) *mgo.Iter {
|
|
return logsCollection.Find(query).Sort("-date").Limit(limit).Batch(8).Iter()
|
|
}
|
|
|
|
// MakeLogID generates log IDs that are of the format from logbot2, though it will break compatibility.
|
|
func MakeLogID(date time.Time, channel string) string {
|
|
return fmt.Sprintf("%s%03d_%s", date.UTC().Format("2006-01-02_150405"), (date.Nanosecond() / int(time.Millisecond/time.Nanosecond)), channel[1:])
|
|
}
|
|
|
|
func init() {
|
|
store.HandleInit(func(db *mgo.Database) {
|
|
logsCollection = db.C("logbot3.logs")
|
|
|
|
logsCollection.EnsureIndexKey("date")
|
|
logsCollection.EnsureIndexKey("channel")
|
|
logsCollection.EnsureIndexKey("characterIds")
|
|
logsCollection.EnsureIndexKey("event")
|
|
logsCollection.EnsureIndex(mgo.Index{
|
|
Key: []string{"channel", "open"},
|
|
})
|
|
err := logsCollection.EnsureIndex(mgo.Index{
|
|
Key: []string{"shortId"},
|
|
Unique: true,
|
|
DropDups: true,
|
|
})
|
|
if err != nil {
|
|
log.Fatalln("init logbot3.logs:", err)
|
|
}
|
|
})
|
|
}
|