This commit is contained in:
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user