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