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.
		
		
		
		
		
			
		
			
				
					
					
						
							253 lines
						
					
					
						
							6.2 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							253 lines
						
					
					
						
							6.2 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 == 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) | |
| } | |
| 
 | |
| // 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, tags []Tag, earliest, latest time.Time, limit int) ([]Story, error) { | |
| 	query := bson.M{} | |
| 
 | |
| 	if author != "" { | |
| 		query["author"] = author | |
| 	} | |
| 
 | |
| 	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, | |
| 		} | |
| 	} | |
| 
 | |
| 	stories := make([]Story, 0, 128) | |
| 	err := storyCollection.Find(query).Limit(limit).One(&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") | |
| 	}) | |
| }
 |