From 8c8a2510804f65c899f32e3e3f65cadb19fb56f8 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 7 Oct 2018 12:18:55 +0200 Subject: [PATCH] package: Removed old models packages that are ported over to the new format. --- model/channel/channel.go | 159 ---------------- model/log/filter.go | 11 -- model/log/log.go | 396 --------------------------------------- model/log/post.go | 198 -------------------- model/log/unknownnick.go | 42 ----- model/log/updater.go | 84 --------- model/story/chapter.go | 111 ----------- model/story/story.go | 275 --------------------------- model/story/tag-kind.go | 58 ------ model/story/tag.go | 36 ---- 10 files changed, 1370 deletions(-) delete mode 100644 model/channel/channel.go delete mode 100644 model/log/filter.go delete mode 100644 model/log/log.go delete mode 100644 model/log/post.go delete mode 100644 model/log/unknownnick.go delete mode 100644 model/log/updater.go delete mode 100644 model/story/chapter.go delete mode 100644 model/story/story.go delete mode 100644 model/story/tag-kind.go delete mode 100644 model/story/tag.go diff --git a/model/channel/channel.go b/model/channel/channel.go deleted file mode 100644 index 093d660..0000000 --- a/model/channel/channel.go +++ /dev/null @@ -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") - }) -} diff --git a/model/log/filter.go b/model/log/filter.go deleted file mode 100644 index ad30ed2..0000000 --- a/model/log/filter.go +++ /dev/null @@ -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 -} diff --git a/model/log/log.go b/model/log/log.go deleted file mode 100644 index da5443f..0000000 --- a/model/log/log.go +++ /dev/null @@ -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) - } - }) -} diff --git a/model/log/post.go b/model/log/post.go deleted file mode 100644 index d30aa63..0000000 --- a/model/log/post.go +++ /dev/null @@ -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) - } - }) -} diff --git a/model/log/unknownnick.go b/model/log/unknownnick.go deleted file mode 100644 index ef8dfe3..0000000 --- a/model/log/unknownnick.go +++ /dev/null @@ -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") - }) -} diff --git a/model/log/updater.go b/model/log/updater.go deleted file mode 100644 index 981fa7c..0000000 --- a/model/log/updater.go +++ /dev/null @@ -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 -} diff --git a/model/story/chapter.go b/model/story/chapter.go deleted file mode 100644 index 6198d6d..0000000 --- a/model/story/chapter.go +++ /dev/null @@ -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") - }) -} diff --git a/model/story/story.go b/model/story/story.go deleted file mode 100644 index 7cd9ccf..0000000 --- a/model/story/story.go +++ /dev/null @@ -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") - }) -} diff --git a/model/story/tag-kind.go b/model/story/tag-kind.go deleted file mode 100644 index d40211e..0000000 --- a/model/story/tag-kind.go +++ /dev/null @@ -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(), "\"") -} diff --git a/model/story/tag.go b/model/story/tag.go deleted file mode 100644 index ff6c9c6..0000000 --- a/model/story/tag.go +++ /dev/null @@ -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 -}