GraphQL API and utilities for the rpdata project
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.
 
 

345 lines
8.3 KiB

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")
})
}