Gisle Aune
6 years ago
14 changed files with 287 additions and 364 deletions
-
12cmd/rpdata-server/main.go
-
64graph2/queries/post.go
-
13graph2/schema/root.gql
-
12graph2/schema/types/Post.gql
-
0internal/counter/counter.go
-
3internal/loader/character.go
-
345model/character/character.go
-
11models/character.go
-
2models/characters/add.go
-
51models/posts/add.go
-
44models/posts/edit.go
-
69models/posts/move.go
-
24models/posts/remove.go
-
1models/posts/search.go
@ -1,345 +0,0 @@ |
|||
package character |
|||
|
|||
import ( |
|||
"errors" |
|||
"log" |
|||
"strconv" |
|||
"strings" |
|||
|
|||
"git.aiterp.net/rpdata/api/internal/store" |
|||
"git.aiterp.net/rpdata/api/model/counter" |
|||
"github.com/globalsign/mgo" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
var collection *mgo.Collection |
|||
|
|||
var logsCollection *mgo.Collection |
|||
|
|||
// Character is a common data model representing an RP character or NPC.
|
|||
type Character struct { |
|||
ID string `json:"id" bson:"_id"` |
|||
Nicks []string `json:"nicks" bson:"nicks"` |
|||
Name string `json:"name" bson:"name"` |
|||
ShortName string `json:"shortName" bson:"shortName"` |
|||
Author string `json:"author" bson:"author"` |
|||
Description string `json:"description" bson:"description"` |
|||
} |
|||
|
|||
// Filter is used to filter the list of characters
|
|||
type Filter struct { |
|||
IDs []string `json:"ids"` |
|||
Nicks []string `json:"nicks"` |
|||
Names []string `json:"names"` |
|||
Author *string `json:"author"` |
|||
Search *string `json:"search"` |
|||
Logged *bool `json:"logged"` |
|||
} |
|||
|
|||
// Nick gets the character's nick.
|
|||
func (character *Character) Nick() *string { |
|||
if len(character.Nicks[0]) == 0 { |
|||
return nil |
|||
} |
|||
|
|||
return &character.Nicks[0] |
|||
} |
|||
|
|||
// HasNick returns true if the character has that nick
|
|||
func (character *Character) HasNick(nick string) bool { |
|||
for i := range character.Nicks { |
|||
if strings.EqualFold(character.Nicks[i], nick) { |
|||
return true |
|||
} |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
// AddNick adds a nick to the character. It will return an error
|
|||
// if the nick already exists.
|
|||
func (character *Character) AddNick(nick string) error { |
|||
for i := range character.Nicks { |
|||
if strings.EqualFold(character.Nicks[i], nick) { |
|||
return errors.New("Nick already exists") |
|||
} |
|||
} |
|||
|
|||
err := collection.UpdateId(character.ID, bson.M{"$push": bson.M{"nicks": nick}}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
character.Nicks = append(character.Nicks, nick) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// RemoveNick removes the nick from the character. It will raise
|
|||
// an error if the nick does not exist; even if that kind of is
|
|||
// the end goal.
|
|||
func (character *Character) RemoveNick(nick string) error { |
|||
index := -1 |
|||
for i := range character.Nicks { |
|||
if strings.EqualFold(character.Nicks[i], nick) { |
|||
index = i |
|||
break |
|||
} |
|||
} |
|||
if index == -1 { |
|||
return errors.New("Nick does not exist") |
|||
} |
|||
|
|||
err := collection.UpdateId(character.ID, bson.M{"$pull": bson.M{"nicks": nick}}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
character.Nicks = append(character.Nicks[:index], character.Nicks[index+1:]...) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Edit sets the fields of metadata. Only non-empty and different fields will be set in the
|
|||
// database, preventing out of order edits to two fields from conflicting
|
|||
func (character *Character) Edit(name, shortName, description string) error { |
|||
changes := bson.M{} |
|||
if len(name) > 0 && name != character.Name { |
|||
changes["name"] = name |
|||
} |
|||
if len(shortName) > 0 && shortName != character.ShortName { |
|||
changes["shortName"] = shortName |
|||
} |
|||
if len(description) > 0 && description != character.Description { |
|||
changes["description"] = description |
|||
} |
|||
|
|||
err := collection.UpdateId(character.ID, changes) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if changes["name"] != nil { |
|||
character.Name = name |
|||
} |
|||
if changes["shortName"] != nil { |
|||
character.ShortName = shortName |
|||
} |
|||
if changes["description"] != nil { |
|||
character.Description = description |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Remove removes the character from the database. The reason this is an instance method
|
|||
// is that it should only be done after an authorization check.
|
|||
func (character *Character) Remove() error { |
|||
return collection.RemoveId(character.ID) |
|||
} |
|||
|
|||
// FindID finds Character by ID
|
|||
func FindID(id string) (Character, error) { |
|||
return find(bson.M{"_id": id}) |
|||
} |
|||
|
|||
// FindNick finds Character by nick
|
|||
func FindNick(nick string) (Character, error) { |
|||
return find(bson.M{"nicks": nick}) |
|||
} |
|||
|
|||
// FindName finds Character by either full name or
|
|||
// short name.
|
|||
func FindName(name string) (Character, error) { |
|||
return find(bson.M{"$or": []bson.M{bson.M{"name": name}, bson.M{"shortName": name}}}) |
|||
} |
|||
|
|||
// List lists all characters
|
|||
func List(filter *Filter) ([]Character, error) { |
|||
query := bson.M{} |
|||
|
|||
if filter != nil { |
|||
if len(filter.IDs) > 1 { |
|||
query["id"] = bson.M{"$in": filter.IDs} |
|||
} else if len(filter.IDs) == 1 { |
|||
query["id"] = filter.IDs[0] |
|||
} |
|||
|
|||
if len(filter.Nicks) > 1 { |
|||
query["nicks"] = bson.M{"$in": filter.Nicks} |
|||
} else if len(filter.Nicks) == 1 { |
|||
query["nicks"] = filter.Nicks[0] |
|||
} |
|||
|
|||
if len(filter.Names) > 1 { |
|||
query["$or"] = bson.M{ |
|||
"name": bson.M{"$in": filter.Names}, |
|||
"shortName": bson.M{"$in": filter.Names}, |
|||
} |
|||
} else if len(filter.Names) == 1 { |
|||
query["$or"] = bson.M{ |
|||
"name": filter.Names[0], |
|||
"shortName": filter.Names[0], |
|||
} |
|||
} |
|||
|
|||
if filter.Logged != nil { |
|||
query["logged"] = *filter.Logged |
|||
} |
|||
|
|||
if filter.Author != nil { |
|||
query["author"] = *filter.Author |
|||
} |
|||
|
|||
if filter.Search != nil { |
|||
query["$text"] = bson.M{"$search": *filter.Search} |
|||
} |
|||
} |
|||
|
|||
return list(query) |
|||
} |
|||
|
|||
// ListAuthor lists all characters by author
|
|||
func ListAuthor(author string) ([]Character, error) { |
|||
return list(bson.M{"author": author}) |
|||
} |
|||
|
|||
// ListNicks lists all characters with either of these nicks. This was made with
|
|||
// the logbot in mind, to batch an order for characters.
|
|||
func ListNicks(nicks ...string) ([]Character, error) { |
|||
return list(bson.M{"nicks": bson.M{"$in": nicks}}) |
|||
} |
|||
|
|||
// ListIDs lists all characters with either of these IDs.
|
|||
func ListIDs(ids ...string) ([]Character, error) { |
|||
return list(bson.M{"_id": bson.M{"$in": ids}}) |
|||
} |
|||
|
|||
// ListFilter lists all logs matching the filters.
|
|||
func ListFilter(ids []string, nicks []string, names []string, author *string, search *string, logged *bool) ([]Character, error) { |
|||
query := bson.M{} |
|||
|
|||
if logged != nil { |
|||
loggedIDs := make([]string, 0, 64) |
|||
err := logsCollection.Find(bson.M{"characterIds": bson.M{"$ne": nil}}).Distinct("characterIds", &loggedIDs) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
if len(ids) > 0 { |
|||
newIds := make([]string, 0, len(ids)) |
|||
for _, id := range ids { |
|||
for _, loggedID := range loggedIDs { |
|||
if id == loggedID { |
|||
newIds = append(newIds, id) |
|||
break |
|||
} |
|||
} |
|||
} |
|||
ids = newIds |
|||
} else { |
|||
ids = loggedIDs |
|||
} |
|||
} |
|||
if len(ids) > 0 { |
|||
query["_id"] = bson.M{"$in": ids} |
|||
} |
|||
if len(nicks) > 0 { |
|||
query["nicks"] = bson.M{"$in": nicks} |
|||
} |
|||
if len(names) > 0 { |
|||
query["name"] = bson.M{"$in": names} |
|||
} |
|||
if author != nil { |
|||
query["author"] = *author |
|||
} |
|||
if search != nil { |
|||
query["$text"] = bson.M{"$search": *search} |
|||
} |
|||
|
|||
return list(query) |
|||
} |
|||
|
|||
// New creates a Character and pushes it to the database. It does some validation
|
|||
// on nick, name, shortName and author. Leave the shortname blank to have it be the
|
|||
// first name.
|
|||
func New(nick, name, shortName, author, description string) (Character, error) { |
|||
if len(nick) < 1 || len(name) < 1 || len(author) < 1 { |
|||
return Character{}, errors.New("Nick, name, or author name too short or empty") |
|||
} |
|||
if shortName == "" { |
|||
shortName = strings.SplitN(name, " ", 2)[0] |
|||
} |
|||
|
|||
char, err := FindNick(nick) |
|||
if err == nil && char.ID != "" { |
|||
return Character{}, errors.New("Nick is occupied") |
|||
} |
|||
|
|||
nextID, err := counter.Next("auto_increment", "Character") |
|||
if err != nil { |
|||
return Character{}, err |
|||
} |
|||
|
|||
character := Character{ |
|||
ID: "C" + strconv.Itoa(nextID), |
|||
Nicks: []string{nick}, |
|||
Name: name, |
|||
ShortName: shortName, |
|||
Author: author, |
|||
Description: description, |
|||
} |
|||
|
|||
err = collection.Insert(character) |
|||
if err != nil { |
|||
return Character{}, err |
|||
} |
|||
|
|||
return character, nil |
|||
} |
|||
|
|||
func find(query interface{}) (Character, error) { |
|||
character := Character{} |
|||
err := collection.Find(query).One(&character) |
|||
if err != nil { |
|||
return Character{}, err |
|||
} |
|||
|
|||
return character, nil |
|||
} |
|||
|
|||
func list(query interface{}) ([]Character, error) { |
|||
characters := make([]Character, 0, 64) |
|||
err := collection.Find(query).All(&characters) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return characters, nil |
|||
} |
|||
|
|||
func init() { |
|||
store.HandleInit(func(db *mgo.Database) { |
|||
collection = db.C("common.characters") |
|||
|
|||
collection.EnsureIndexKey("name") |
|||
collection.EnsureIndexKey("shortName") |
|||
collection.EnsureIndexKey("author") |
|||
err := collection.EnsureIndex(mgo.Index{ |
|||
Key: []string{"nicks"}, |
|||
Unique: true, |
|||
DropDups: true, |
|||
}) |
|||
if err != nil { |
|||
log.Fatalln("init common.characters:", err) |
|||
} |
|||
err = collection.EnsureIndex(mgo.Index{ |
|||
Key: []string{"$text:description"}, |
|||
}) |
|||
if err != nil { |
|||
log.Fatalln("init common.characters:", err) |
|||
} |
|||
|
|||
logsCollection = db.C("logbot3.logs") |
|||
}) |
|||
} |
@ -0,0 +1,51 @@ |
|||
package posts |
|||
|
|||
import ( |
|||
"crypto/rand" |
|||
"encoding/binary" |
|||
"errors" |
|||
"strconv" |
|||
"time" |
|||
|
|||
"git.aiterp.net/rpdata/api/internal/counter" |
|||
"git.aiterp.net/rpdata/api/models" |
|||
) |
|||
|
|||
// Add creates a new post.
|
|||
func Add(log models.Log, time time.Time, kind, nick, text string) (models.Post, error) { |
|||
if kind == "" || nick == "" || text == "" { |
|||
return models.Post{}, errors.New("Missing/empty parameters") |
|||
} |
|||
|
|||
mutex.RLock() |
|||
defer mutex.RUnlock() |
|||
|
|||
position, err := counter.Next("next_post_id", log.ShortID) |
|||
if err != nil { |
|||
return models.Post{}, err |
|||
} |
|||
|
|||
post := models.Post{ |
|||
ID: generateID(time), |
|||
Position: position, |
|||
LogID: log.ShortID, |
|||
Time: time, |
|||
Kind: kind, |
|||
Nick: nick, |
|||
Text: text, |
|||
} |
|||
|
|||
err = collection.Insert(post) |
|||
if err != nil { |
|||
return models.Post{}, err |
|||
} |
|||
|
|||
return post, nil |
|||
} |
|||
|
|||
func generateID(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) |
|||
} |
@ -0,0 +1,44 @@ |
|||
package posts |
|||
|
|||
import ( |
|||
"time" |
|||
|
|||
"git.aiterp.net/rpdata/api/models" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
// Edit edits a post and returns the result if the edit succeeded.
|
|||
func Edit(post models.Post, time *time.Time, kind *string, nick *string, text *string) (models.Post, error) { |
|||
mutex.RLock() |
|||
defer mutex.RUnlock() |
|||
|
|||
changes := bson.M{} |
|||
|
|||
if time != nil && !time.IsZero() && !time.Equal(post.Time) { |
|||
changes["time"] = *time |
|||
post.Time = *time |
|||
} |
|||
if kind != nil && *kind != "" && *kind != post.Kind { |
|||
changes["kind"] = *kind |
|||
post.Kind = *kind |
|||
} |
|||
if nick != nil && *nick != "" && *nick != post.Nick { |
|||
changes["nick"] = *nick |
|||
post.Nick = *nick |
|||
} |
|||
if text != nil && *text != "" && *text != post.Text { |
|||
changes["text"] = *text |
|||
post.Text = *text |
|||
} |
|||
|
|||
if len(changes) == 0 { |
|||
return post, nil |
|||
} |
|||
|
|||
err := collection.UpdateId(post.ID, bson.M{"$set": changes}) |
|||
if err != nil { |
|||
return models.Post{}, err |
|||
} |
|||
|
|||
return post, nil |
|||
} |
@ -0,0 +1,69 @@ |
|||
package posts |
|||
|
|||
import ( |
|||
"errors" |
|||
|
|||
"git.aiterp.net/rpdata/api/models" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
// Move the post
|
|||
func Move(post models.Post, toPosition int) ([]models.Post, error) { |
|||
if toPosition < 1 { |
|||
return nil, errors.New("Invalid position") |
|||
} |
|||
|
|||
mutex.Lock() |
|||
defer mutex.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 { |
|||
existing := models.Post{} |
|||
err := collection.Find(bson.M{"logId": post.LogID, "position": toPosition}).One(&existing) |
|||
|
|||
if err != nil || existing.Position != toPosition { |
|||
return nil, 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 := collection.UpdateAll(query, operation) |
|||
if err != nil { |
|||
return nil, errors.New("Moving others failed: " + err.Error()) |
|||
} |
|||
|
|||
err = collection.UpdateId(post.ID, bson.M{"$set": bson.M{"position": toPosition}}) |
|||
if err != nil { |
|||
return nil, errors.New("Moving failed: " + err.Error() + " (If you see this on the page, please let me know ASAP)") |
|||
} |
|||
|
|||
from, to := post.Position, toPosition |
|||
if to < from { |
|||
from, to = to, from |
|||
} |
|||
|
|||
posts := make([]models.Post, 0, (to-from)+1) |
|||
err = collection.Find(bson.M{"logId": post.LogID, "position": bson.M{"$gte": from, "$lte": to}}).Sort("position").All(&posts) |
|||
if err != nil { |
|||
return nil, errors.New("The move completed successfully, but finding the moved posts failed: " + err.Error()) |
|||
} |
|||
|
|||
return posts, nil |
|||
} |
@ -0,0 +1,24 @@ |
|||
package posts |
|||
|
|||
import ( |
|||
"git.aiterp.net/rpdata/api/models" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
// Remove removes a post, moving all subsequent post up one position
|
|||
func Remove(post models.Post) (models.Post, error) { |
|||
mutex.Lock() |
|||
defer mutex.Unlock() |
|||
|
|||
err := collection.RemoveId(post.ID) |
|||
if err != nil { |
|||
return models.Post{}, err |
|||
} |
|||
|
|||
_, err = collection.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}}) |
|||
if err != nil { |
|||
return models.Post{}, err |
|||
} |
|||
|
|||
return post, nil |
|||
} |
@ -1 +0,0 @@ |
|||
package posts |
Write
Preview
Loading…
Cancel
Save
Reference in new issue