You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
6.6 KiB
270 lines
6.6 KiB
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
|
|
}
|
|
|
|
stories := make([]Story, 0, 128)
|
|
err := storyCollection.Find(query).Limit(limit).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")
|
|
})
|
|
}
|