Gisle Aune
6 years ago
10 changed files with 0 additions and 1370 deletions
-
159model/channel/channel.go
-
11model/log/filter.go
-
396model/log/log.go
-
198model/log/post.go
-
42model/log/unknownnick.go
-
84model/log/updater.go
-
111model/story/chapter.go
-
275model/story/story.go
-
58model/story/tag-kind.go
-
36model/story/tag.go
@ -1,159 +0,0 @@ |
|||
package channel |
|||
|
|||
import ( |
|||
"errors" |
|||
"strings" |
|||
|
|||
"git.aiterp.net/rpdata/api/internal/store" |
|||
"github.com/globalsign/mgo" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
var collection *mgo.Collection |
|||
|
|||
// ErrInvalidName is an error for an invalid channel name
|
|||
var ErrInvalidName = errors.New("Invalid channel name") |
|||
|
|||
// A Channel represents information abount an IRC RP channel, and whether it should be logged
|
|||
type Channel struct { |
|||
Name string `bson:"_id"` |
|||
Logged bool `bson:"logged"` |
|||
Hub bool `bson:"hub"` |
|||
EventName string `bson:"event,omitempty"` |
|||
LocationName string `bson:"location,omitempty"` |
|||
} |
|||
|
|||
// Filter for searching
|
|||
type Filter struct { |
|||
Logged *bool `json:"logged"` |
|||
EventName string `json:"eventName"` |
|||
LocationName string `json:"locationName"` |
|||
} |
|||
|
|||
// Edit edits the channel
|
|||
func (channel *Channel) Edit(logged, hub *bool, event, location *string) error { |
|||
changes := bson.M{} |
|||
changed := *channel |
|||
|
|||
if logged != nil && channel.Logged != *logged { |
|||
changes["logged"] = *logged |
|||
changed.Logged = *logged |
|||
} |
|||
if hub != nil && channel.Hub != *hub { |
|||
changes["hub"] = *hub |
|||
changed.Hub = *hub |
|||
} |
|||
if event != nil && channel.EventName != *event { |
|||
changes["event"] = *event |
|||
changed.EventName = *event |
|||
} |
|||
if location != nil && channel.LocationName != *location { |
|||
changes["location"] = *location |
|||
changed.LocationName = *location |
|||
} |
|||
|
|||
if len(changes) == 0 { |
|||
return nil |
|||
} |
|||
|
|||
err := collection.UpdateId(channel.Name, bson.M{"$set": changes}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
*channel = changed |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Remove removes the channel information from the database.
|
|||
func (channel *Channel) Remove() error { |
|||
return collection.RemoveId(channel.Name) |
|||
} |
|||
|
|||
// Ensure ensures a channel's existence. It does not change `logged` if there is
|
|||
// an existing channel.
|
|||
func Ensure(name string, logged bool) (Channel, error) { |
|||
channel, err := FindName(name) |
|||
if err == mgo.ErrNotFound { |
|||
return New(name, logged, false, "", "") |
|||
} else if err != nil { |
|||
return Channel{}, err |
|||
} |
|||
|
|||
return channel, nil |
|||
} |
|||
|
|||
// New creates a new channel
|
|||
func New(name string, logged, hub bool, event, location string) (Channel, error) { |
|||
if len(name) < 3 && !strings.HasPrefix(name, "#") { |
|||
return Channel{}, ErrInvalidName |
|||
} |
|||
|
|||
channel := Channel{ |
|||
Name: name, |
|||
Logged: logged, |
|||
Hub: hub, |
|||
EventName: event, |
|||
LocationName: location, |
|||
} |
|||
|
|||
err := collection.Insert(channel) |
|||
if err != nil { |
|||
return Channel{}, err |
|||
} |
|||
|
|||
return channel, nil |
|||
} |
|||
|
|||
// FindName finds a channel by its id (its name).
|
|||
func FindName(name string) (Channel, error) { |
|||
channel := Channel{} |
|||
err := collection.FindId(name).One(&channel) |
|||
|
|||
return channel, err |
|||
} |
|||
|
|||
// List finds channels, if logged is true it will be limited to logged
|
|||
// channels
|
|||
func List(filter *Filter) ([]Channel, error) { |
|||
query := bson.M{} |
|||
|
|||
if filter != nil { |
|||
if filter.Logged != nil { |
|||
query["logged"] = *filter.Logged |
|||
} |
|||
if filter.EventName != "" { |
|||
query["eventName"] = filter.EventName |
|||
} |
|||
if filter.LocationName != "" { |
|||
query["locationName"] = filter.LocationName |
|||
} |
|||
} |
|||
|
|||
channels := make([]Channel, 0, 128) |
|||
err := collection.Find(query).All(&channels) |
|||
|
|||
return channels, err |
|||
} |
|||
|
|||
// ListNames finds channels by the names provided
|
|||
func ListNames(names ...string) ([]Channel, error) { |
|||
query := bson.M{"_id": bson.M{"$in": names}} |
|||
|
|||
channels := make([]Channel, 0, 32) |
|||
err := collection.Find(query).All(&channels) |
|||
|
|||
return channels, err |
|||
} |
|||
|
|||
func init() { |
|||
store.HandleInit(func(db *mgo.Database) { |
|||
collection = db.C("common.channels") |
|||
|
|||
collection.EnsureIndexKey("logged") |
|||
collection.EnsureIndexKey("hub") |
|||
collection.EnsureIndexKey("event") |
|||
collection.EnsureIndexKey("location") |
|||
}) |
|||
} |
@ -1,11 +0,0 @@ |
|||
package log |
|||
|
|||
// Filter for the List() function
|
|||
type Filter struct { |
|||
Search *string |
|||
Characters *[]string |
|||
Channels *[]string |
|||
Events *[]string |
|||
Open *bool |
|||
Limit int |
|||
} |
@ -1,396 +0,0 @@ |
|||
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"` |
|||
ChannelName 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, |
|||
ChannelName: 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(filter *Filter) ([]Log, error) { |
|||
query := bson.M{} |
|||
limit := 0 |
|||
|
|||
if filter != nil { |
|||
// Run a text search
|
|||
if filter.Search != nil { |
|||
searchResults := make([]string, 0, 32) |
|||
|
|||
postMutex.RLock() |
|||
err := postCollection.Find(bson.M{"$text": bson.M{"$search": *filter.Search}}).Distinct("logId", &searchResults) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
postMutex.RUnlock() |
|||
|
|||
// 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 filter.Channels != nil { |
|||
query["channel"] = bson.M{"$in": *filter.Channels} |
|||
} |
|||
if filter.Events != nil { |
|||
query["event"] = bson.M{"$in": *filter.Events} |
|||
} |
|||
|
|||
// Find logs including all of the specified character IDs.
|
|||
if filter.Characters != nil { |
|||
query["characterIds"] = bson.M{"$all": *filter.Characters} |
|||
} |
|||
|
|||
// Limit to only open logs
|
|||
if filter.Open != nil { |
|||
query["open"] = *filter.Open |
|||
} |
|||
|
|||
// Set the limit from the filter
|
|||
limit = filter.Limit |
|||
} |
|||
|
|||
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 |
|||
} |
|||
|
|||
// Characters get all the characters for the character IDs stored in the
|
|||
// log file.
|
|||
func (log *Log) Characters() ([]character.Character, error) { |
|||
return character.ListIDs(log.CharacterIDs...) |
|||
} |
|||
|
|||
// Channel gets the channel.
|
|||
func (log *Log) Channel() (channel.Channel, error) { |
|||
return channel.FindName(log.ChannelName) |
|||
} |
|||
|
|||
// 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([]string{"action", "text", "chars"}) |
|||
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 |
|||
} |
|||
|
|||
// Remove removes the log and all associated posts from the database
|
|||
func (log *Log) Remove() error { |
|||
err := logsCollection.Remove(bson.M{"_id": log.ID}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
_, err = postCollection.RemoveAll(bson.M{"$or": []bson.M{ |
|||
bson.M{"logId": log.ID}, |
|||
bson.M{"logId": log.ShortID}, |
|||
}}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
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) |
|||
} |
|||
}) |
|||
} |
@ -1,198 +0,0 @@ |
|||
package log |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"encoding/binary" |
|||
"errors" |
|||
"log" |
|||
"strconv" |
|||
"time" |
|||
|
|||
"git.aiterp.net/rpdata/api/internal/store" |
|||
"github.com/globalsign/mgo" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
var postCollection *mgo.Collection |
|||
|
|||
// A Post is a part of a log file.
|
|||
type Post struct { |
|||
ID string `bson:"_id"` |
|||
LogID string `bson:"logId"` |
|||
Time time.Time `bson:"time"` |
|||
Kind string `bson:"kind"` |
|||
Nick string `bson:"nick"` |
|||
Text string `bson:"text"` |
|||
Position int `bson:"position"` |
|||
} |
|||
|
|||
// Edit the post
|
|||
func (post *Post) Edit(time *time.Time, kind *string, nick *string, text *string) error { |
|||
changes := bson.M{} |
|||
changed := false |
|||
postCopy := *post |
|||
|
|||
if time != nil && !time.IsZero() && !time.Equal(post.Time) { |
|||
changes["time"] = *time |
|||
changed = true |
|||
postCopy.Time = *time |
|||
} |
|||
if kind != nil && *kind != "" && *kind != post.Kind { |
|||
changes["kind"] = *kind |
|||
changed = true |
|||
postCopy.Kind = *kind |
|||
} |
|||
if nick != nil && *nick != "" && *nick != post.Nick { |
|||
changes["nick"] = *nick |
|||
changed = true |
|||
postCopy.Nick = *nick |
|||
} |
|||
if text != nil && *text != "" && *text != post.Text { |
|||
changes["text"] = *text |
|||
changed = true |
|||
postCopy.Text = *text |
|||
} |
|||
|
|||
if !changed { |
|||
return nil |
|||
} |
|||
|
|||
err := postCollection.UpdateId(post.ID, bson.M{"$set": changes}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
*post = postCopy |
|||
return nil |
|||
} |
|||
|
|||
// Move the post
|
|||
func (post *Post) Move(toPosition int) error { |
|||
if toPosition < 1 { |
|||
return errors.New("Invalid position") |
|||
} |
|||
|
|||
postMutex.Lock() |
|||
defer postMutex.Unlock() |
|||
|
|||
// To avoid problems, only allow target indices that are allowed. If it's 1, then there is bound to
|
|||
// be a post at the position.
|
|||
if toPosition > 1 { |
|||
existingPost := Post{} |
|||
err := postCollection.Find(bson.M{"logId": post.LogID, "position": toPosition}).One(&existingPost) |
|||
|
|||
if err != nil || existingPost.Position != toPosition { |
|||
return errors.New("No post found at the position") |
|||
} |
|||
} |
|||
|
|||
query := bson.M{"logId": post.LogID} |
|||
operation := bson.M{"$inc": bson.M{"position": 1}} |
|||
|
|||
if toPosition < post.Position { |
|||
query["$and"] = []bson.M{ |
|||
bson.M{"position": bson.M{"$gte": toPosition}}, |
|||
bson.M{"position": bson.M{"$lt": post.Position}}, |
|||
} |
|||
} else { |
|||
query["$and"] = []bson.M{ |
|||
bson.M{"position": bson.M{"$gt": post.Position}}, |
|||
bson.M{"position": bson.M{"$lte": toPosition}}, |
|||
} |
|||
|
|||
operation["$inc"] = bson.M{"position": -1} |
|||
} |
|||
|
|||
_, err := postCollection.UpdateAll(query, operation) |
|||
if err != nil { |
|||
return errors.New("moving others: " + err.Error()) |
|||
} |
|||
|
|||
err = postCollection.UpdateId(post.ID, bson.M{"$set": bson.M{"position": toPosition}}) |
|||
if err != nil { |
|||
return errors.New("moving: " + err.Error()) |
|||
} |
|||
|
|||
post.Position = toPosition |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// FindPostID finds a log post by ID.
|
|||
func FindPostID(id string) (Post, error) { |
|||
return findPost(bson.M{"_id": id}) |
|||
} |
|||
|
|||
// ListPostIDs lists log posts by ID
|
|||
func ListPostIDs(ids ...string) ([]Post, error) { |
|||
return listPosts(bson.M{"_id": bson.M{"$in": ids}}) |
|||
} |
|||
|
|||
// RemovePost removes a post, moving all subsequent post up one position
|
|||
func RemovePost(id string) (Post, error) { |
|||
postMutex.Lock() |
|||
defer postMutex.Unlock() |
|||
|
|||
post, err := findPost(bson.M{"_id": id}) |
|||
if err != nil { |
|||
return Post{}, err |
|||
} |
|||
|
|||
err = postCollection.RemoveId(id) |
|||
if err != nil { |
|||
return Post{}, err |
|||
} |
|||
|
|||
_, err = postCollection.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}}) |
|||
if err != nil { |
|||
return Post{}, err |
|||
} |
|||
|
|||
return post, nil |
|||
} |
|||
|
|||
func findPost(query interface{}) (Post, error) { |
|||
post := Post{} |
|||
err := postCollection.Find(query).One(&post) |
|||
if err != nil { |
|||
return Post{}, err |
|||
} |
|||
|
|||
return post, nil |
|||
} |
|||
|
|||
func listPosts(query interface{}) ([]Post, error) { |
|||
posts := make([]Post, 0, 64) |
|||
err := postCollection.Find(query).All(&posts) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return posts, nil |
|||
} |
|||
|
|||
// MakePostID makes a random post ID
|
|||
func MakePostID(time time.Time) string { |
|||
data := make([]byte, 4) |
|||
rand.Read(data) |
|||
|
|||
return "P" + strconv.FormatInt(time.UnixNano(), 36) + strconv.FormatInt(int64(binary.LittleEndian.Uint32(data)), 36) |
|||
} |
|||
|
|||
func init() { |
|||
store.HandleInit(func(db *mgo.Database) { |
|||
postCollection = db.C("logbot3.posts") |
|||
|
|||
postCollection.EnsureIndexKey("logId") |
|||
postCollection.EnsureIndexKey("time") |
|||
postCollection.EnsureIndexKey("kind") |
|||
postCollection.EnsureIndexKey("position") |
|||
|
|||
err := postCollection.EnsureIndex(mgo.Index{ |
|||
Key: []string{"$text:text"}, |
|||
}) |
|||
if err != nil { |
|||
log.Fatalln("init logbot3.logs:", err) |
|||
} |
|||
}) |
|||
} |
@ -1,42 +0,0 @@ |
|||
package log |
|||
|
|||
import ( |
|||
"git.aiterp.net/rpdata/api/internal/store" |
|||
"github.com/globalsign/mgo" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
var unknownConnection *mgo.Collection |
|||
|
|||
// An UnknownNick is a nick found by the character list updater that
|
|||
// does not exist. The score is the number of logs that nick was in, meaning
|
|||
// nicks with a higher score should be a high priority to be matched with
|
|||
// a character.
|
|||
type UnknownNick struct { |
|||
Nick string `bson:"_id" json:"nick"` |
|||
Score int `bson:"score" json:"score"` |
|||
} |
|||
|
|||
// UnknownNicks gets all the unknown nicks from the last search.
|
|||
func UnknownNicks() ([]UnknownNick, error) { |
|||
nicks := make([]UnknownNick, 0, 256) |
|||
err := unknownConnection.Find(bson.M{}).Sort("-score").All(&nicks) |
|||
|
|||
return nicks, err |
|||
} |
|||
|
|||
func addUnknownNick(nick string) error { |
|||
_, err := unknownConnection.UpsertId(nick, bson.M{"$inc": bson.M{"score": 1}}) |
|||
return err |
|||
} |
|||
|
|||
func clearUnknownNicks() error { |
|||
_, err := unknownConnection.RemoveAll(bson.M{}) |
|||
return err |
|||
} |
|||
|
|||
func init() { |
|||
store.HandleInit(func(db *mgo.Database) { |
|||
unknownConnection = db.C("logbot3.unknown_nicks") |
|||
}) |
|||
} |
@ -1,84 +0,0 @@ |
|||
package log |
|||
|
|||
import ( |
|||
"sync" |
|||
"time" |
|||
|
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
var scheduleCharacterUpdate = func() func() { |
|||
var mutex sync.Mutex |
|||
var scheduled bool |
|||
|
|||
return func() { |
|||
mutex.Lock() |
|||
if !scheduled { |
|||
go func() { |
|||
time.Sleep(time.Second * 60) |
|||
|
|||
// If another comes along in the next 2-3 seconds, it should schedule a new
|
|||
// round to avoid a character only appearing in half their logs.
|
|||
mutex.Lock() |
|||
scheduled = false |
|||
mutex.Unlock() |
|||
|
|||
UpdateAllCharacters() |
|||
}() |
|||
|
|||
scheduled = true |
|||
} |
|||
mutex.Unlock() |
|||
} |
|||
}() |
|||
|
|||
// ScheduleCharacterUpdate schedules a full update within the minute.
|
|||
// Subsequent calls within that time will not schedule anything. Even
|
|||
// if the operation takes a few seconds at most, it need not be ran often.
|
|||
func ScheduleCharacterUpdate() { |
|||
scheduleCharacterUpdate() |
|||
} |
|||
|
|||
// UpdateCharacters is a shorthand for getting a log and updaing its characters
|
|||
func UpdateCharacters(logID string) error { |
|||
log, err := FindID(logID) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return log.UpdateCharacters() |
|||
} |
|||
|
|||
// UpdateAllCharacters updates character list on all logs. This should
|
|||
// be done if one or more characters failed to be added.
|
|||
func UpdateAllCharacters() (updated int, err error) { |
|||
updated = 0 |
|||
|
|||
err = clearUnknownNicks() |
|||
if err != nil { |
|||
return |
|||
} |
|||
|
|||
iter := iterLogs(bson.M{}, 0) |
|||
err = iter.Err() |
|||
if err != nil { |
|||
return |
|||
} |
|||
|
|||
log := Log{} |
|||
for iter.Next(&log) { |
|||
err = log.UpdateCharacters() |
|||
if err != nil { |
|||
return |
|||
} |
|||
|
|||
updated++ |
|||
} |
|||
|
|||
err = iter.Err() |
|||
if err != nil { |
|||
return |
|||
} |
|||
|
|||
return |
|||
} |
@ -1,111 +0,0 @@ |
|||
package story |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"encoding/binary" |
|||
"strconv" |
|||
"time" |
|||
|
|||
"git.aiterp.net/rpdata/api/internal/store" |
|||
"github.com/globalsign/mgo" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
var chapterCollection *mgo.Collection |
|||
|
|||
// A Chapter is a part of a story.
|
|||
type Chapter struct { |
|||
ID string `bson:"_id"` |
|||
StoryID string `bson:"storyId"` |
|||
Title string `bson:"title"` |
|||
Author string `bson:"author"` |
|||
Source string `bson:"source"` |
|||
CreatedDate time.Time `bson:"createdDate"` |
|||
FictionalDate time.Time `bson:"fictionalDate,omitempty"` |
|||
EditedDate time.Time `bson:"editedDate"` |
|||
} |
|||
|
|||
// Edit edits a chapter, and updates EditedDate. While many Edit functions cheat if there's nothing to
|
|||
// change, this functill will due to EditedDate.
|
|||
func (chapter *Chapter) Edit(title, source *string, fictionalDate *time.Time) error { |
|||
now := time.Now() |
|||
changes := bson.M{"editedDate": now} |
|||
changed := *chapter |
|||
changed.EditedDate = now |
|||
|
|||
if title != nil && *title != chapter.Title { |
|||
changes["title"] = *title |
|||
changed.Title = *title |
|||
} |
|||
if source != nil && *source != chapter.Source { |
|||
changes["source"] = *source |
|||
changed.Source = *source |
|||
} |
|||
if fictionalDate != nil && !fictionalDate.Equal(chapter.FictionalDate) { |
|||
changes["fictionalDate"] = *fictionalDate |
|||
changed.FictionalDate = *fictionalDate |
|||
} |
|||
|
|||
err := chapterCollection.UpdateId(chapter.ID, bson.M{"$set": changes}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
*chapter = changed |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Remove removes a chapter.
|
|||
func (chapter *Chapter) Remove() error { |
|||
return chapterCollection.RemoveId(chapter.ID) |
|||
} |
|||
|
|||
// FindChapterID finds a chapter by its own ID
|
|||
func FindChapterID(id string) (Chapter, error) { |
|||
chapter := Chapter{} |
|||
err := chapterCollection.FindId(id).One(&chapter) |
|||
|
|||
return chapter, err |
|||
} |
|||
|
|||
// ListChapterStoryID lists all chapters for the story ID
|
|||
func ListChapterStoryID(storyID string) ([]Chapter, error) { |
|||
chapters := make([]Chapter, 0, 8) |
|||
err := chapterCollection.Find(bson.M{"storyId": storyID}).Sort("createdDate").All(&chapters) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return chapters, nil |
|||
} |
|||
|
|||
// makeChapterID makes a random chapter ID that's 24 characters long
|
|||
func makeChapterID() string { |
|||
result := "SC" |
|||
offset := 0 |
|||
data := make([]byte, 32) |
|||
|
|||
rand.Read(data) |
|||
for len(result) < 24 { |
|||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36) |
|||
offset += 8 |
|||
|
|||
if offset >= 32 { |
|||
rand.Read(data) |
|||
offset = 0 |
|||
} |
|||
} |
|||
|
|||
return result[:24] |
|||
} |
|||
|
|||
func init() { |
|||
store.HandleInit(func(db *mgo.Database) { |
|||
chapterCollection = db.C("story.chapters") |
|||
|
|||
chapterCollection.EnsureIndexKey("storyId") |
|||
chapterCollection.EnsureIndexKey("author") |
|||
chapterCollection.EnsureIndexKey("createdDate") |
|||
}) |
|||
} |
@ -1,275 +0,0 @@ |
|||
package story |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"encoding/binary" |
|||
"errors" |
|||
"fmt" |
|||
"os" |
|||
"strconv" |
|||
"time" |
|||
|
|||
"git.aiterp.net/rpdata/api/internal/store" |
|||
"github.com/globalsign/mgo" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
var storyCollection *mgo.Collection |
|||
|
|||
// ErrTagAlreadyExists is an error returned by Story.AddTag
|
|||
var ErrTagAlreadyExists = errors.New("Tag already exists") |
|||
|
|||
// ErrTagNotExists is an error returned by Story.RemoveTag
|
|||
var ErrTagNotExists = errors.New("Tag does not exist") |
|||
|
|||
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
|
|||
// The story model is a container for multiple chapters this time, in contrast to the previous version.
|
|||
type Story struct { |
|||
ID string `bson:"_id"` |
|||
Author string `bson:"author"` |
|||
Name string `bson:"name"` |
|||
Category string `bson:"category"` |
|||
Open bool `bson:"open"` |
|||
Listed bool `bson:"listed"` |
|||
Tags []Tag `bson:"tags"` |
|||
CreatedDate time.Time `bson:"createdDate"` |
|||
FictionalDate time.Time `bson:"fictionalDate,omitempty"` |
|||
UpdatedDate time.Time `bson:"updatedDate"` |
|||
} |
|||
|
|||
// AddTag adds a tag to the story. It returns ErrTagAlreadyExists if the tag is already there
|
|||
func (story *Story) AddTag(tag Tag) error { |
|||
for i := range story.Tags { |
|||
if story.Tags[i].Equal(tag) { |
|||
return ErrTagAlreadyExists |
|||
} |
|||
} |
|||
|
|||
err := storyCollection.UpdateId(story.ID, bson.M{"$push": bson.M{"tags": tag}}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
story.Tags = append(story.Tags, tag) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// RemoveTag removes a tag to the story. It returns ErrTagNotExists if the tag does not exist.
|
|||
func (story *Story) RemoveTag(tag Tag) error { |
|||
index := -1 |
|||
for i := range story.Tags { |
|||
if story.Tags[i].Equal(tag) { |
|||
index = i |
|||
break |
|||
} |
|||
} |
|||
if index == -1 { |
|||
return ErrTagNotExists |
|||
} |
|||
|
|||
err := storyCollection.UpdateId(story.ID, bson.M{"$pull": bson.M{"tags": tag}}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
story.Tags = append(story.Tags[:index], story.Tags[index+1:]...) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Edit edits the story, reflecting the new values in the story's struct values. If nothing will be
|
|||
// changed, it will silently return without a database roundtrip.
|
|||
func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDate *time.Time) error { |
|||
changes := bson.M{} |
|||
changed := *story |
|||
|
|||
if name != nil && *name != story.Name { |
|||
changes["name"] = *name |
|||
changed.Name = *name |
|||
} |
|||
if category != nil && *category != story.Category { |
|||
changes["category"] = *category |
|||
changed.Name = *category |
|||
} |
|||
if listed != nil && *listed != story.Listed { |
|||
changes["listed"] = *listed |
|||
changed.Listed = *listed |
|||
} |
|||
if open != nil && *open != story.Open { |
|||
changes["open"] = *open |
|||
changed.Open = *open |
|||
} |
|||
if fictionalDate != nil && !fictionalDate.Equal(story.FictionalDate) { |
|||
changes["fictionalDate"] = *fictionalDate |
|||
changed.FictionalDate = *fictionalDate |
|||
} |
|||
|
|||
if len(changes) == 0 { |
|||
return nil |
|||
} |
|||
|
|||
err := storyCollection.UpdateId(story.ID, bson.M{"$set": changes}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
*story = changed |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Remove the story from the database
|
|||
func (story *Story) Remove() error { |
|||
return storyCollection.RemoveId(story.ID) |
|||
} |
|||
|
|||
// Chapters calls ListChapterStoryID with the story's ID:
|
|||
func (story *Story) Chapters() ([]Chapter, error) { |
|||
return ListChapterStoryID(story.ID) |
|||
} |
|||
|
|||
// AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
|
|||
// occurs
|
|||
func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) { |
|||
if !story.Open && author != story.Author { |
|||
fmt.Fprintf(os.Stderr, "WARNING: AddChapter is breaking Open rules (story.id=%#+v, story.name=%#+v, chapter.author=%#+v, chapter.title=%#+v)", story.ID, story.Name, author, title) |
|||
} |
|||
|
|||
chapter := Chapter{ |
|||
ID: makeChapterID(), |
|||
StoryID: story.ID, |
|||
Title: title, |
|||
Author: author, |
|||
Source: source, |
|||
CreatedDate: createdDate, |
|||
FictionalDate: finctionalDate, |
|||
EditedDate: createdDate, |
|||
} |
|||
|
|||
err := chapterCollection.Insert(chapter) |
|||
if err != nil { |
|||
return Chapter{}, err |
|||
} |
|||
|
|||
if createdDate.After(story.UpdatedDate) { |
|||
if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil { |
|||
story.UpdatedDate = createdDate |
|||
} |
|||
} |
|||
|
|||
return chapter, nil |
|||
} |
|||
|
|||
// New creates a new story.
|
|||
func New(name, author, category string, listed, open bool, tags []Tag, createdDate, fictionalDate time.Time) (Story, error) { |
|||
story := Story{ |
|||
ID: makeStoryID(), |
|||
Name: name, |
|||
Author: author, |
|||
Category: category, |
|||
Listed: listed, |
|||
Open: open, |
|||
Tags: tags, |
|||
CreatedDate: createdDate, |
|||
FictionalDate: fictionalDate, |
|||
UpdatedDate: createdDate, |
|||
} |
|||
|
|||
err := storyCollection.Insert(story) |
|||
if err != nil { |
|||
return Story{}, err |
|||
} |
|||
|
|||
return story, nil |
|||
} |
|||
|
|||
// FindID finds a story by ID
|
|||
func FindID(id string) (Story, error) { |
|||
story := Story{} |
|||
err := storyCollection.FindId(id).One(&story) |
|||
|
|||
return story, err |
|||
} |
|||
|
|||
// List lists stories by any non-zero criteria passed with it.
|
|||
func List(author string, category string, tags []Tag, earliest, latest time.Time, unlisted bool, open *bool, limit int) ([]Story, error) { |
|||
query := bson.M{} |
|||
|
|||
if author != "" { |
|||
query["author"] = author |
|||
} |
|||
|
|||
if category != "" { |
|||
query["category"] = category |
|||
} |
|||
|
|||
if len(tags) > 0 { |
|||
query["tags"] = bson.M{"$in": tags} |
|||
} |
|||
|
|||
if !earliest.IsZero() && !latest.IsZero() { |
|||
query["fictionalDate"] = bson.M{ |
|||
"$gte": earliest, |
|||
"$lt": latest, |
|||
} |
|||
} else if !latest.IsZero() { |
|||
query["fictionalDate"] = bson.M{ |
|||
"$lt": latest, |
|||
} |
|||
} else if !earliest.IsZero() { |
|||
query["fictionalDate"] = bson.M{ |
|||
"$gte": earliest, |
|||
} |
|||
} |
|||
|
|||
if unlisted { |
|||
query["listed"] = false |
|||
} |
|||
|
|||
if open != nil { |
|||
query["open"] = *open |
|||
} |
|||
|
|||
size := limit |
|||
if size == 0 { |
|||
size = 128 |
|||
} |
|||
stories := make([]Story, 0, size) |
|||
|
|||
err := storyCollection.Find(query).Limit(limit).Sort("-updatedDate").All(&stories) |
|||
|
|||
return stories, err |
|||
} |
|||
|
|||
// makeStoryID makes a random story ID that's 16 characters long
|
|||
func makeStoryID() string { |
|||
result := "S" |
|||
offset := 0 |
|||
data := make([]byte, 32) |
|||
|
|||
rand.Read(data) |
|||
for len(result) < 16 { |
|||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36) |
|||
offset += 8 |
|||
|
|||
if offset >= 32 { |
|||
rand.Read(data) |
|||
offset = 0 |
|||
} |
|||
} |
|||
|
|||
return result[:16] |
|||
} |
|||
|
|||
func init() { |
|||
store.HandleInit(func(db *mgo.Database) { |
|||
storyCollection = db.C("story.stories") |
|||
|
|||
storyCollection.EnsureIndexKey("tags") |
|||
storyCollection.EnsureIndexKey("author") |
|||
storyCollection.EnsureIndexKey("updatedDate") |
|||
storyCollection.EnsureIndexKey("fictionalDate") |
|||
storyCollection.EnsureIndexKey("listed") |
|||
}) |
|||
} |
@ -1,58 +0,0 @@ |
|||
package story |
|||
|
|||
import ( |
|||
"fmt" |
|||
"io" |
|||
) |
|||
|
|||
// TagKind represents the kind of tags.
|
|||
type TagKind string |
|||
|
|||
const ( |
|||
// TagKindOrganization is a tag kind, see GraphQL documentation.
|
|||
TagKindOrganization TagKind = "Organization" |
|||
|
|||
// TagKindCharacter is a tag kind, see GraphQL documentation.
|
|||
TagKindCharacter TagKind = "Character" |
|||
|
|||
// TagKindLocation is a tag kind, see GraphQL documentation.
|
|||
TagKindLocation TagKind = "Location" |
|||
|
|||
// TagKindEvent is a tag kind, see GraphQL documentation.
|
|||
TagKindEvent TagKind = "Event" |
|||
|
|||
// TagKindSeries is a tag kind, see GraphQL documentation.
|
|||
TagKindSeries TagKind = "Series" |
|||
) |
|||
|
|||
// IsValid returns true if the TagKind is one of the constants
|
|||
func (e TagKind) IsValid() bool { |
|||
switch e { |
|||
case TagKindOrganization, TagKindCharacter, TagKindLocation, TagKindEvent, TagKindSeries: |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
|
|||
func (e TagKind) String() string { |
|||
return string(e) |
|||
} |
|||
|
|||
// UnmarshalGQL unmarshals
|
|||
func (e *TagKind) UnmarshalGQL(v interface{}) error { |
|||
str, ok := v.(string) |
|||
if !ok { |
|||
return fmt.Errorf("enums must be strings") |
|||
} |
|||
|
|||
*e = TagKind(str) |
|||
if !e.IsValid() { |
|||
return fmt.Errorf("%s is not a valid TagKind", str) |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
// MarshalGQL turns it into a JSON string
|
|||
func (e TagKind) MarshalGQL(w io.Writer) { |
|||
fmt.Fprint(w, "\""+e.String(), "\"") |
|||
} |
@ -1,36 +0,0 @@ |
|||
package story |
|||
|
|||
import ( |
|||
"sort" |
|||
"strings" |
|||
|
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
// A Tag associates a story with other content, like other stories, logs and more.
|
|||
type Tag struct { |
|||
Kind TagKind `bson:"kind"` |
|||
Name string `bson:"name"` |
|||
} |
|||
|
|||
// Equal returns true if the tags match one another.
|
|||
func (tag *Tag) Equal(other Tag) bool { |
|||
return tag.Kind == other.Kind && tag.Name == other.Name |
|||
} |
|||
|
|||
// ListTags lists all tags
|
|||
func ListTags() ([]Tag, error) { |
|||
tags := make([]Tag, 0, 64) |
|||
err := storyCollection.Find(bson.M{"listed": true, "tags": bson.M{"$ne": nil}}).Distinct("tags", &tags) |
|||
|
|||
sort.Slice(tags, func(i, j int) bool { |
|||
kindCmp := strings.Compare(string(tags[i].Kind), string(tags[j].Kind)) |
|||
if kindCmp != 0 { |
|||
return kindCmp < 0 |
|||
} |
|||
|
|||
return strings.Compare(tags[i].Name, tags[j].Name) < 0 |
|||
}) |
|||
|
|||
return tags, err |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue