11 changed files with 429 additions and 0 deletions
			
			
		- 
					11graph2/gqlgen.yml
 - 
					4graph2/graph.go
 - 
					31graph2/queries/story.go
 - 
					7graph2/schema/root.gql
 - 
					151graph2/schema/types/Story.gql
 - 
					27graph2/types/story.go
 - 
					59models/stories/db.go
 - 
					11models/stories/find.go
 - 
					67models/stories/list.go
 - 
					43models/story-category.go
 - 
					18models/story.go
 
@ -0,0 +1,31 @@ | 
				
			|||
package queries | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"context" | 
				
			|||
	"errors" | 
				
			|||
 | 
				
			|||
	"git.aiterp.net/rpdata/api/internal/auth" | 
				
			|||
	"git.aiterp.net/rpdata/api/models" | 
				
			|||
	"git.aiterp.net/rpdata/api/models/stories" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
func (r *resolver) Story(ctx context.Context, id string) (models.Story, error) { | 
				
			|||
	return stories.FindID(id) | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func (r *resolver) Stories(ctx context.Context, filter *stories.Filter) ([]models.Story, error) { | 
				
			|||
	if filter != nil { | 
				
			|||
		if filter.Unlisted != nil && *filter.Unlisted == true { | 
				
			|||
			token := auth.TokenFromContext(ctx) | 
				
			|||
			if !token.Authenticated() { | 
				
			|||
				return nil, errors.New("You are not permitted to view unlisted stories") | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			if !token.Permitted("story.unlisted") { | 
				
			|||
				filter.Author = &token.UserID | 
				
			|||
			} | 
				
			|||
		} | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return stories.List(filter) | 
				
			|||
} | 
				
			|||
@ -0,0 +1,151 @@ | 
				
			|||
# A Story is a piece of content that, unlike wiki articles, are not something that should be ordered by time instead of title. It can | 
				
			|||
# contain multiple chapters by one or more autohrs | 
				
			|||
type Story { | 
				
			|||
  # The story's ID | 
				
			|||
  id: String! | 
				
			|||
 | 
				
			|||
  # The story's name, which will show up in the lists. | 
				
			|||
  name: String! | 
				
			|||
 | 
				
			|||
  # The story's author | 
				
			|||
  author: String! | 
				
			|||
 | 
				
			|||
  # Whether other users may add/edit chapters to the story. | 
				
			|||
  open: Boolean! | 
				
			|||
 | 
				
			|||
  # Whether users without a direct link can find this story. | 
				
			|||
  listed: Boolean! | 
				
			|||
 | 
				
			|||
  # The category of the story | 
				
			|||
  category: StoryCategory! | 
				
			|||
 | 
				
			|||
  # The tags for this tory | 
				
			|||
  tags: [Tag!]! | 
				
			|||
 | 
				
			|||
  # The chapters of this story | 
				
			|||
  chapters: [Chapter!]! | 
				
			|||
 | 
				
			|||
  # The date the story was created. | 
				
			|||
  createdDate: Date! | 
				
			|||
 | 
				
			|||
  # The date the story is set in. | 
				
			|||
  fictionalDate: Date | 
				
			|||
 | 
				
			|||
  # The date of the last major update to the story. This being bumped means a new chapter has been added. | 
				
			|||
  updatedDate: Date! | 
				
			|||
} | 
				
			|||
 | 
				
			|||
# Filter for stories query. | 
				
			|||
input StoriesFilter { | 
				
			|||
  # What author to query for | 
				
			|||
  author: String | 
				
			|||
 | 
				
			|||
  # What category to query for | 
				
			|||
  category: StoryCategory | 
				
			|||
 | 
				
			|||
  # What tags to query for | 
				
			|||
  tags: [TagInput!] | 
				
			|||
 | 
				
			|||
  # If true, it will only show unlisted page you have access to | 
				
			|||
  unlisted: Boolean | 
				
			|||
 | 
				
			|||
  # Whether the page is open for additions by other users | 
				
			|||
  open: Boolean | 
				
			|||
 | 
				
			|||
  # The earliest fictionalDate | 
				
			|||
  earliestFictionalDate: Date | 
				
			|||
 | 
				
			|||
  # The latest fictionalDate | 
				
			|||
  latestFictionalDate: Date | 
				
			|||
 | 
				
			|||
  # The max amount of stories to get (default: 30) | 
				
			|||
  limit: Int | 
				
			|||
} | 
				
			|||
 | 
				
			|||
# Input for the addStory mutation | 
				
			|||
input StoryAddInput {  | 
				
			|||
  # Set the name of the story, which will show up as the title, and as a suggestion for | 
				
			|||
  # the first chapter being added. | 
				
			|||
  name: String! | 
				
			|||
 | 
				
			|||
  # Set the category for the new story. | 
				
			|||
  category: StoryCategory! | 
				
			|||
 | 
				
			|||
  # Add the story under another name. This requires the story.add permission, and should not be | 
				
			|||
  # abused. The action will be logged with the actual creator's user ID. | 
				
			|||
  author: String | 
				
			|||
 | 
				
			|||
  # Allow other users to post chapters to the story. | 
				
			|||
  open: Boolean | 
				
			|||
 | 
				
			|||
  # Allow other users to see this page in any lists or search results. | 
				
			|||
  listed: Boolean | 
				
			|||
 | 
				
			|||
  # Set which tags the story should have. | 
				
			|||
  tags: [TagInput!] | 
				
			|||
 | 
				
			|||
  # Set the fictional date of the story. | 
				
			|||
  fictionalDate: Date | 
				
			|||
} | 
				
			|||
 | 
				
			|||
# Input for the editStory mutation | 
				
			|||
input StoryEditInput { | 
				
			|||
  # What story to edit | 
				
			|||
  id: String! | 
				
			|||
 | 
				
			|||
  # Set the name of the story, which will show up as the title, and as a suggestion for | 
				
			|||
  # the first chapter being added. | 
				
			|||
  name: String | 
				
			|||
 | 
				
			|||
  # Set the category for the new story. | 
				
			|||
  category: StoryCategory | 
				
			|||
 | 
				
			|||
  # Add the story under another name. This requires the story.add permission, and should not be | 
				
			|||
  # abused. The action will be logged with the actual creator's user ID. | 
				
			|||
  author: String | 
				
			|||
 | 
				
			|||
  # Change whether to allow others to add chapters | 
				
			|||
  open: Boolean | 
				
			|||
 | 
				
			|||
  # Change whether to show this story in the list and search results | 
				
			|||
  listed: Boolean | 
				
			|||
 | 
				
			|||
  # Set the fictional date of the story. | 
				
			|||
  fictionalDate: Date | 
				
			|||
} | 
				
			|||
 | 
				
			|||
# Input for the addStoryTag mutation | 
				
			|||
input StoryTagAddInput { | 
				
			|||
  # What story to add the tag to. | 
				
			|||
  id: String! | 
				
			|||
 | 
				
			|||
  # The tag to add. | 
				
			|||
  tag: TagInput! | 
				
			|||
} | 
				
			|||
 | 
				
			|||
# Input for the removeStoryTag mutation | 
				
			|||
input StoryTagRemoveInput { | 
				
			|||
  # What story to remove the tag from. | 
				
			|||
  id: String! | 
				
			|||
 | 
				
			|||
  # The tag to remove. | 
				
			|||
  tag: TagInput! | 
				
			|||
} | 
				
			|||
 | 
				
			|||
# Possible values for Story.category | 
				
			|||
enum StoryCategory { | 
				
			|||
  # General information | 
				
			|||
  Info | 
				
			|||
 | 
				
			|||
  # News stories | 
				
			|||
  News | 
				
			|||
 | 
				
			|||
  # Description and content of a document or item | 
				
			|||
  Document | 
				
			|||
 | 
				
			|||
  # Information about something going on in the background that may or may not inform RP | 
				
			|||
  Background | 
				
			|||
 | 
				
			|||
  # A short story | 
				
			|||
  Story | 
				
			|||
} | 
				
			|||
@ -0,0 +1,27 @@ | 
				
			|||
package types | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"context" | 
				
			|||
	"time" | 
				
			|||
 | 
				
			|||
	"git.aiterp.net/rpdata/api/models/chapters" | 
				
			|||
 | 
				
			|||
	"git.aiterp.net/rpdata/api/models" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
type storyResolver struct{} | 
				
			|||
 | 
				
			|||
func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) { | 
				
			|||
	if story.FictionalDate.IsZero() { | 
				
			|||
		return nil, nil | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return &story.FictionalDate, nil | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]models.Chapter, error) { | 
				
			|||
	return chapters.ListStoryID(story.ID) | 
				
			|||
} | 
				
			|||
 | 
				
			|||
// StoryResolver is a resolver
 | 
				
			|||
var StoryResolver storyResolver | 
				
			|||
@ -0,0 +1,59 @@ | 
				
			|||
package stories | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"crypto/rand" | 
				
			|||
	"encoding/binary" | 
				
			|||
	"strconv" | 
				
			|||
 | 
				
			|||
	"git.aiterp.net/rpdata/api/internal/store" | 
				
			|||
	"git.aiterp.net/rpdata/api/models" | 
				
			|||
	"github.com/globalsign/mgo" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
var collection *mgo.Collection | 
				
			|||
 | 
				
			|||
func find(query interface{}) (models.Story, error) { | 
				
			|||
	story := models.Story{} | 
				
			|||
	err := collection.Find(query).One(&story) | 
				
			|||
 | 
				
			|||
	return story, err | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func list(query interface{}, limit int) ([]models.Story, error) { | 
				
			|||
	stories := make([]models.Story, 0, 64) | 
				
			|||
	err := collection.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) { | 
				
			|||
		collection = db.C("story.stories") | 
				
			|||
 | 
				
			|||
		collection.EnsureIndexKey("tags") | 
				
			|||
		collection.EnsureIndexKey("author") | 
				
			|||
		collection.EnsureIndexKey("updatedDate") | 
				
			|||
		collection.EnsureIndexKey("fictionalDate") | 
				
			|||
		collection.EnsureIndexKey("listed") | 
				
			|||
	}) | 
				
			|||
} | 
				
			|||
@ -0,0 +1,11 @@ | 
				
			|||
package stories | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"git.aiterp.net/rpdata/api/models" | 
				
			|||
	"github.com/globalsign/mgo/bson" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// FindID finds a story by ID
 | 
				
			|||
func FindID(id string) (models.Story, error) { | 
				
			|||
	return find(bson.M{"_id": id}) | 
				
			|||
} | 
				
			|||
@ -0,0 +1,67 @@ | 
				
			|||
package stories | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"time" | 
				
			|||
 | 
				
			|||
	"git.aiterp.net/rpdata/api/models" | 
				
			|||
	"github.com/globalsign/mgo/bson" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// Filter for stories.List
 | 
				
			|||
type Filter struct { | 
				
			|||
	Author                *string | 
				
			|||
	Tags                  []models.Tag | 
				
			|||
	EarliestFictionalDate time.Time | 
				
			|||
	LatestFictionalDate   time.Time | 
				
			|||
	Category              *models.StoryCategory | 
				
			|||
	Open                  *bool | 
				
			|||
	Unlisted              *bool | 
				
			|||
	Limit                 int | 
				
			|||
} | 
				
			|||
 | 
				
			|||
// List lists stories by any non-zero criteria passed with it.
 | 
				
			|||
func List(filter *Filter) ([]models.Story, error) { | 
				
			|||
	query := bson.M{"listed": true} | 
				
			|||
	limit := 0 | 
				
			|||
 | 
				
			|||
	if filter != nil { | 
				
			|||
		if filter.Author != nil { | 
				
			|||
			query["author"] = *filter.Author | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		if len(filter.Tags) > 0 { | 
				
			|||
			query["tags"] = bson.M{"$in": filter.Tags} | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		if !filter.EarliestFictionalDate.IsZero() && !filter.LatestFictionalDate.IsZero() { | 
				
			|||
			query["fictionalDate"] = bson.M{ | 
				
			|||
				"$gte": filter.EarliestFictionalDate, | 
				
			|||
				"$lt":  filter.LatestFictionalDate, | 
				
			|||
			} | 
				
			|||
		} else if !filter.LatestFictionalDate.IsZero() { | 
				
			|||
			query["fictionalDate"] = bson.M{ | 
				
			|||
				"$lt": filter.LatestFictionalDate, | 
				
			|||
			} | 
				
			|||
		} else if !filter.EarliestFictionalDate.IsZero() { | 
				
			|||
			query["fictionalDate"] = bson.M{ | 
				
			|||
				"$gte": filter.EarliestFictionalDate, | 
				
			|||
			} | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		if filter.Category != nil { | 
				
			|||
			query["category"] = *filter.Category | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		if filter.Open != nil { | 
				
			|||
			query["open"] = *filter.Open | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		if filter.Unlisted != nil { | 
				
			|||
			query["listed"] = !*filter.Unlisted | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		limit = filter.Limit | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return list(query, limit) | 
				
			|||
} | 
				
			|||
@ -0,0 +1,43 @@ | 
				
			|||
package models | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"fmt" | 
				
			|||
	"io" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// StoryCategory represents the category of a story.
 | 
				
			|||
type StoryCategory string | 
				
			|||
 | 
				
			|||
const ( | 
				
			|||
	// StoryCategoryInfo is a story category, see GraphQL documentation.
 | 
				
			|||
	StoryCategoryInfo StoryCategory = "Info" | 
				
			|||
	// StoryCategoryNews is a story category, see GraphQL documentation.
 | 
				
			|||
	StoryCategoryNews StoryCategory = "News" | 
				
			|||
	// StoryCategoryDocument is a story category, see GraphQL documentation.
 | 
				
			|||
	StoryCategoryDocument StoryCategory = "Document" | 
				
			|||
	// StoryCategoryBackground is a story category, see GraphQL documentation.
 | 
				
			|||
	StoryCategoryBackground StoryCategory = "Background" | 
				
			|||
	// StoryCategoryStory is a story category, see GraphQL documentation.
 | 
				
			|||
	StoryCategoryStory StoryCategory = "Story" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// UnmarshalGQL unmarshals
 | 
				
			|||
func (e *StoryCategory) UnmarshalGQL(v interface{}) error { | 
				
			|||
	str, ok := v.(string) | 
				
			|||
	if !ok { | 
				
			|||
		return fmt.Errorf("enums must be strings") | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	*e = StoryCategory(str) | 
				
			|||
	switch *e { | 
				
			|||
	case StoryCategoryInfo, StoryCategoryNews, StoryCategoryDocument, StoryCategoryBackground, StoryCategoryStory: | 
				
			|||
		return nil | 
				
			|||
	default: | 
				
			|||
		return fmt.Errorf("%s is not a valid StoryCategory", str) | 
				
			|||
	} | 
				
			|||
} | 
				
			|||
 | 
				
			|||
// MarshalGQL turns it into a JSON string
 | 
				
			|||
func (e StoryCategory) MarshalGQL(w io.Writer) { | 
				
			|||
	fmt.Fprint(w, "\""+string(e)+"\"") | 
				
			|||
} | 
				
			|||
@ -0,0 +1,18 @@ | 
				
			|||
package models | 
				
			|||
 | 
				
			|||
import "time" | 
				
			|||
 | 
				
			|||
// 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      StoryCategory `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"` | 
				
			|||
} | 
				
			|||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue