Browse Source

package: Removed old models packages that are ported over to the new format.

1.0
Gisle Aune 6 years ago
parent
commit
8c8a251080
  1. 159
      model/channel/channel.go
  2. 11
      model/log/filter.go
  3. 396
      model/log/log.go
  4. 198
      model/log/post.go
  5. 42
      model/log/unknownnick.go
  6. 84
      model/log/updater.go
  7. 111
      model/story/chapter.go
  8. 275
      model/story/story.go
  9. 58
      model/story/tag-kind.go
  10. 36
      model/story/tag.go

159
model/channel/channel.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")
})
}

11
model/log/filter.go

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

396
model/log/log.go

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

198
model/log/post.go

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

42
model/log/unknownnick.go

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

84
model/log/updater.go

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

111
model/story/chapter.go

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

275
model/story/story.go

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

58
model/story/tag-kind.go

@ -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(), "\"")
}

36
model/story/tag.go

@ -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
}
Loading…
Cancel
Save