|
|
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) } }) }
|