Browse Source

graph2: Added post mutations. general: Did some cleaning.

1.0
Gisle Aune 6 years ago
parent
commit
bb28795cd5
  1. 12
      cmd/rpdata-server/main.go
  2. 64
      graph2/queries/post.go
  3. 13
      graph2/schema/root.gql
  4. 12
      graph2/schema/types/Post.gql
  5. 0
      internal/counter/counter.go
  6. 3
      internal/loader/character.go
  7. 345
      model/character/character.go
  8. 11
      models/character.go
  9. 2
      models/characters/add.go
  10. 51
      models/posts/add.go
  11. 44
      models/posts/edit.go
  12. 69
      models/posts/move.go
  13. 24
      models/posts/remove.go
  14. 1
      models/posts/search.go

12
cmd/rpdata-server/main.go

@ -11,7 +11,6 @@ import (
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/internal/loader"
"git.aiterp.net/rpdata/api/internal/store"
logModel "git.aiterp.net/rpdata/api/model/log"
"github.com/99designs/gqlgen/handler"
)
@ -24,8 +23,6 @@ func main() {
http.Handle("/", handler.Playground("RPData API", "/graphql"))
http.Handle("/graphql", queryHandler())
go updateCharacters()
log.Fatal(http.ListenAndServe(":8081", nil))
}
@ -50,12 +47,3 @@ func queryHandler() http.HandlerFunc {
handler.ServeHTTP(w, r)
}
}
func updateCharacters() {
n, err := logModel.UpdateAllCharacters()
if err != nil {
log.Println("Charcter updated stopped:", err)
}
log.Println("Updated characters on", n, "logs")
}

64
graph2/queries/post.go

@ -4,10 +4,16 @@ import (
"context"
"errors"
"git.aiterp.net/rpdata/api/models/logs"
"git.aiterp.net/rpdata/api/graph2/input"
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/posts"
)
// Queries
func (r *resolver) Post(ctx context.Context, id string) (models.Post, error) {
return posts.FindID(id)
}
@ -32,3 +38,61 @@ func (r *resolver) Posts(ctx context.Context, filter *posts.Filter) ([]models.Po
return posts.List(filter)
}
// Mutation
func (r *mutationResolver) AddPost(ctx context.Context, input input.PostAddInput) (models.Post, error) {
token := auth.TokenFromContext(ctx)
if !token.Authenticated() || !token.Permitted("post.add") {
return models.Post{}, errors.New("You are not permitted to edit logs")
}
log, err := logs.FindID(input.LogID)
if err != nil {
return models.Post{}, err
}
return posts.Add(log, input.Time, input.Kind, input.Nick, input.Text)
}
func (r *mutationResolver) EditPost(ctx context.Context, input input.PostEditInput) (models.Post, error) {
token := auth.TokenFromContext(ctx)
if !token.Authenticated() || !token.Permitted("post.edit") {
return models.Post{}, errors.New("You are not permitted to edit logs")
}
post, err := posts.FindID(input.ID)
if err != nil {
return models.Post{}, errors.New("Post not found")
}
return posts.Edit(post, input.Time, input.Kind, input.Nick, input.Text)
}
func (r *mutationResolver) MovePost(ctx context.Context, input input.PostMoveInput) ([]models.Post, error) {
token := auth.TokenFromContext(ctx)
if !token.Authenticated() || !token.Permitted("post.move") {
return nil, errors.New("You are not permitted to edit logs")
}
post, err := posts.FindID(input.ID)
if err != nil {
return nil, errors.New("Post not found")
}
return posts.Move(post, input.ToPosition)
}
func (r *mutationResolver) RemovePost(ctx context.Context, input input.PostRemoveInput) (models.Post, error) {
token := auth.TokenFromContext(ctx)
if !token.Authenticated() || !token.Permitted("post.remove") {
return models.Post{}, errors.New("You are not permitted to edit logs")
}
post, err := posts.FindID(input.ID)
if err != nil {
return models.Post{}, errors.New("Post not found (before removing, of course)")
}
return posts.Remove(post)
}

13
graph2/schema/root.gql

@ -83,6 +83,19 @@ type Mutation {
# Remove a chapter
removeChapter(input: ChapterRemoveInput!): Chapter!
# Add a post
addPost(input: PostAddInput!): Post!
# Edit a post
editPost(input: PostEditInput!): Post!
# Move a post. All affected posts will be returned.
movePost(input: PostMoveInput!): [Post!]!
# Remove a post
removePost(input: PostRemoveInput!): Post!
}
# A Date represents a RFC3339 encoded date with up to millisecond precision.

12
graph2/schema/types/Post.gql

@ -23,7 +23,7 @@ type Post {
}
# Input for the addPost mutation
input AddPostInput {
input PostAddInput {
# The log's ID that this post should be a part of
logId: String!
@ -41,7 +41,7 @@ input AddPostInput {
}
# Input for the editPost mutation
input EditPostInput {
input PostEditInput {
# The Post ID
id: String!
@ -59,7 +59,7 @@ input EditPostInput {
}
# Input for the movePost mutation
input MovePostInput {
input PostMoveInput {
# The Post ID
id: String!
@ -67,6 +67,12 @@ input MovePostInput {
toPosition: Int!
}
# Input for the removePost mutation
input PostRemoveInput {
# The Post ID
id: String!
}
# Filter for posts query
input PostsFilter {
id: [String!]

0
model/counter/counter.go → internal/counter/counter.go

3
internal/loader/character.go

@ -5,7 +5,6 @@ import (
"errors"
"strings"
"git.aiterp.net/rpdata/api/model/character"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/characters"
"github.com/graph-gophers/dataloader"
@ -118,7 +117,7 @@ func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader
var results []*dataloader.Result
nicks := keys.Keys()
characters, err := character.ListNicks(nicks...)
characters, err := characters.List(&characters.Filter{Nicks: nicks})
if err != nil {
for range nicks {
results = append(results, &dataloader.Result{Error: err})

345
model/character/character.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")
})
}

11
models/character.go

@ -18,3 +18,14 @@ func (character *Character) Nick() *string {
return &character.Nicks[0]
}
// HasNick gets whether the character has the nick.
func (character *Character) HasNick(nick string) bool {
for i := range character.Nicks {
if nick == character.Nicks[i] {
return true
}
}
return false
}

2
models/characters/add.go

@ -5,7 +5,7 @@ import (
"strconv"
"strings"
"git.aiterp.net/rpdata/api/model/counter"
"git.aiterp.net/rpdata/api/internal/counter"
"git.aiterp.net/rpdata/api/models"
)

51
models/posts/add.go

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

44
models/posts/edit.go

@ -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
}

69
models/posts/move.go

@ -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
}

24
models/posts/remove.go

@ -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
models/posts/search.go

@ -1 +0,0 @@
package posts
Loading…
Cancel
Save