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

package log
import (
"errors"
"fmt"
"log"
"sort"
"strconv"
"strings"
"sync"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"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, channel, title, event, description string, open bool) (Log, error) {
nextID, err := counter.Next("auto_increment", "Log")
if err != nil {
return Log{}, err
}
log := Log{
ID: MakeLogID(date, channel),
ShortID: "L" + strconv.Itoa(nextID),
Date: date,
Channel: channel,
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].Index < posts[j].Index
})
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()
index, err := counter.Next("next_post_id", log.ShortID)
if err != nil {
return Post{}, err
}
post := Post{
ID: MakePostID(time),
Index: index,
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)
}
})
}