Compare commits

...
40 Commits
Author SHA1 Message Date
gisle ba1c1e0a4b Changed characters graphql query to use the filter format, changed description for files query to match the others'
the build was successful
2018-07-15 17:55:21 +02:00
gisle 18e77b8205 Changed .gitignore to include new bindata.go location 2018-07-15 17:14:35 +02:00
gisle d8871259a1 Changed channels graphql query to use filter, added eventName and locationName filters 2018-07-15 16:58:17 +02:00
gisle 26d7e36631 Changed files graphql query to use a filter argument to look more like other queries. 2018-07-15 15:34:58 +02:00
gisle 51489ed901 Changed log query's ID parameter to be compulsory 2018-07-15 15:16:47 +02:00
gisle ed3e384d55 Moved query and mutation resolvers into individual files
the build was successful
2018-07-15 15:10:35 +02:00
gisle 9455fd960f Moved packages loader, resolver and schema under graphql
the build was successful
2018-07-15 14:10:00 +02:00
gisle 36939ffc59 Moved SessionResolver and UserResolver into resolvers/types package, updated references to it.
the build was successful
2018-07-14 19:14:56 +02:00
gisle 5fedce5899 Moved StoryResolver and TagResolver into resolvers/types package, updated references to it. 2018-07-14 19:13:43 +02:00
gisle dced5282b1 Moved LogResolver and PostResolver into resolvers/types package, updated references to it. 2018-07-14 19:11:04 +02:00
gisle d05f3a8ca8 Moved FileResolver into resolvers/types package, updated references to it.
the build was successful
2018-07-14 18:39:34 +02:00
gisle 53e2dfc6b6 Moved CharacterResolver into resolvers/types package, updated references to it. 2018-07-14 18:37:47 +02:00
gisle fba499024b Moved ChapterResolver into resolvers/types package, updated references to it. 2018-07-14 18:36:08 +02:00
gisle cc765e220a Moved ChannelResolver into resolvers/types package, updated references to it. 2018-07-14 18:33:37 +02:00
gisle ba49b1fd30 Renamed input parameter in stories and logs to filter, made filter optional for stories query 2018-07-14 18:29:00 +02:00
gisle 8c290c8dcd Removed hard-coded sample tags 2018-07-12 22:15:12 +02:00
gisle 576e1df001 UI: Menu and routing works with style reminiscent of aitestory
the build was successful
2018-07-12 22:12:01 +02:00
gisle 7adcdfe1e0 Added change.Timeline, added change submissions to channel mutations, made all change submissions spin off a goroutine
the build was successful
2018-07-08 13:25:18 +02:00
gisle 6f04da49b8 Fixed inconsistencies in code and documentation under resolvers, changed log.Remove to be a method instead of a function as well as take the posts with it.
the build was successful
2018-07-08 12:10:06 +02:00
gisle 9b705e6514 Added change.Submit to addStory, editStory and removeStory
the build was successful
2018-07-08 00:35:15 +02:00
gisle 155309693a Changed removeFile mutation's type to File!
the build was successful
2018-07-08 00:30:07 +02:00
gisle 5895e09f37 Added addStory, addStoryTag, removeStoryTag, editStory and removeStory mutations 2018-07-08 00:28:52 +02:00
gisle 223d9922e9 Changed story.ListTags to ignore tags for unlisted stories 2018-07-07 23:14:17 +02:00
gisle 429387c9ac Added @playlyfe/gql package to UI, added .vscode to gitignore
the build failed
2018-07-07 22:46:54 +02:00
gisle a7113d5efe Added add/edit/remove mutations for Chapter
the build was successful
2018-07-07 17:45:24 +02:00
gisle a0cb8d7fa8 Added tags query 2018-07-07 15:48:14 +02:00
gisle 5189d486a5 Added stories query and resolvers
the build was successful
2018-07-07 15:16:10 +02:00
gisle 011cb00001 Fixed incorrect unmarshalling of results from story.List 2018-07-07 15:15:40 +02:00
gisle 7a52504381 Removed wiki: prefix on authors for imported stories from rpdata-as2storyimport 2018-07-07 15:15:02 +02:00
gisle 866a5056be Added Story.chapters to schema, added Story and Tag resolver
the build was successful
2018-07-07 14:48:15 +02:00
gisle e0409a6635 Fixed all imported stories with rpdata-as2storyimport being unlisted 2018-07-07 13:57:31 +02:00
gisle bae15de3e8 Fixed edit functions not detecting changes correctly 2018-07-07 13:53:11 +02:00
gisle 3f5af44dd6 Fixed error 'Unsupported document type for unmarshalling: []story.Chapter' in story.ListChapterStoryID 2018-07-07 13:48:11 +02:00
gisle 4bbb8bb9ae Fixed typo in commenst for fictionalDate and editedDate resolvers 2018-07-07 13:10:54 +02:00
gisle 87cd5e8e3a Added chapter query and Chapter type resolvers, added functions to get chapters 2018-07-07 12:55:41 +02:00
gisle 136302084d Fixed newline shenanigans in rpdata-as2storyimport
the build was successful
2018-07-07 12:53:52 +02:00
gisle 0122f64fbb Fixed Tags->Tag typo in Story schema
the build was successful
2018-07-07 10:21:10 +02:00
gisle 6bfb9cbe20 Updated packages
the build failed
2018-07-01 21:16:13 +02:00
gisle fca5176579 Changed go base image back to non-alpine version because it lacks git
the build was successful
2018-06-30 20:40:37 +02:00
gisle c4f7846269 Created react app, changed Dockerfile to include UI files
the build failed
2018-06-30 20:38:58 +02:00
118 changed files with 15795 additions and 1415 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
vendor
schema/bindata.go
graphql/schema/bindata.go
config.json
api
debug
build
.vscode
+27 -4
View File
@@ -1,5 +1,6 @@
## 1. Server
# Use Go 1.10
FROM golang:1.10
FROM golang:1.10 AS build-server
# Load repository into docker
WORKDIR /go/src/git.aiterp.net/rpdata/api/
@@ -20,8 +21,30 @@ RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-wikifile
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-ensurechannels ./cmd/rpdata-ensurechannels
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-as2storyimport ./cmd/rpdata-as2storyimport
# Move artifacts into final container
FROM alpine:latest
## 2. React UI
# Use Node.JS 18
FROM node:10.5-alpine AS build-id
# The content is already there
WORKDIR /rpdata-ui
COPY ./rpdata-ui /rpdata-ui
# Prepare dependencies
RUN npm install
# Run build
RUN npm run build
## 3. Regroup
# Use alpine linux
FROM alpine:3.7
# Install certificates to enable HTTPS GET requests
RUN apk add --no-cache ca-certificates
COPY --from=0 /binaries/* /usr/local/bin/
# Copy build files into final container
COPY --from=build-server /binaries/* /usr/local/bin/
COPY --from=build-id /rpdata-ui/build /var/www/
# TODO: Use proper server instead of grapihql server
CMD ["/usr/local/bin/rpdata-graphiql"]
+17 -3
View File
@@ -78,23 +78,37 @@ func main() {
}
}
story, err := story.New(result.Name, result.Author, result.Category, false, false, tags, publishDate, fictionalDate)
story, err := story.New(result.Name, result.Author[5:], result.Category, false, false, tags, publishDate, fictionalDate)
if err != nil {
log.Fatalln(err)
}
// Change the story title
title := result.Name
if strings.HasPrefix(result.Source, "#") {
firstNewline := strings.Index(result.Source, "\n")
title = result.Source[1:firstNewline]
result.Name = strings.Replace(result.Source[1:firstNewline], "\r", "", -1)
result.Source = result.Source[firstNewline+1:]
result.Source = strings.Replace(result.Source, "\r\n", "\n", -1)
if strings.HasPrefix(result.Source, "\r") || strings.HasPrefix(result.Source, "\n") {
result.Source = result.Source[1:]
}
}
chapter, err := story.AddChapter(title, result.Author, result.Source, publishDate, fictionalDate)
chapter, err := story.AddChapter(title, result.Author[5:], result.Source, publishDate, fictionalDate)
if err != nil {
log.Fatalln(err)
}
if !result.Unlisted {
listed := true
err := story.Edit(nil, nil, &listed, nil, nil)
if err != nil {
log.Fatalln(err)
}
}
fmt.Println(result.ID, "->", story.ID, chapter.ID)
}
}
+24 -4
View File
@@ -1,17 +1,19 @@
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"git.aiterp.net/rpdata/api/graphql/loader"
"git.aiterp.net/rpdata/api/graphql/resolver"
"git.aiterp.net/rpdata/api/graphql/schema"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/loader"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/file"
logModel "git.aiterp.net/rpdata/api/model/log"
"git.aiterp.net/rpdata/api/resolver"
"git.aiterp.net/rpdata/api/schema"
graphql "github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/relay"
)
@@ -28,7 +30,7 @@ func main() {
}
log.Println("Updated characters on", n, "logs")
schema, err := graphql.ParseSchema(schema.String(), &resolver.RootResolver{}, graphql.MaxParallelism(48))
schema, err := graphql.ParseSchema(schema.String(), &resolver.Resolver{}, graphql.MaxParallelism(48))
if err != nil {
log.Fatalln("Failed to parse schema:", err)
}
@@ -89,6 +91,24 @@ func main() {
w.Write(json)
})
change.InitializeTimeline()
go func() {
index := int64(0)
for {
index++
ch, err := change.Timeline.Get(context.Background(), index)
if err == change.ErrExpired {
continue
} else if err != nil {
log.Println("Timeline:", err)
break
}
log.Printf("Change: time=%s model=%s op=%s id=%s author=%s\n", ch.Time.Local().Format("[Mon Jan 02 15:04 MST]"), ch.Model, ch.Op, ch.ObjectID, ch.Author)
}
}()
log.Fatal(http.ListenAndServe(":17000", nil))
}
+59
View File
@@ -0,0 +1,59 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/channel"
)
// ChannelAddArgs is input for the addChannel mutation
type ChannelAddArgs struct {
Input *struct {
Name string
Logged *bool
Hub *bool
EventName *string
LocationName *string
}
}
// AddChannel resolves the addChannel mutation
func (r *MutationResolver) AddChannel(ctx context.Context, args *ChannelAddArgs) (*types.ChannelResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("channel.add") {
return nil, ErrUnauthorized
}
logged := input.Logged != nil && *input.Logged
hub := input.Hub != nil && *input.Hub
eventName := ""
if input.EventName != nil {
eventName = *input.EventName
}
locationName := ""
if input.LocationName != nil {
locationName = *input.LocationName
}
channel, err := channel.New(input.Name, logged, hub, eventName, locationName)
if err != nil {
return nil, err
}
go change.Submit("Channel", "add", user.ID, channel.Name, map[string]interface{}{
"logged": channel.Logged,
"hub": channel.Hub,
"location": input.LocationName,
"event": input.EventName,
})
return &types.ChannelResolver{C: channel}, nil
}
+67
View File
@@ -0,0 +1,67 @@
package mutations
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// AddChapterArgs is args for the addChapter mutation
type AddChapterArgs struct {
Input *struct {
StoryID string
Title string
Author *string
Source string
FictionalDate *string
}
}
// AddChapter resolves the addChapter mutation
func (r *MutationResolver) AddChapter(ctx context.Context, args *AddChapterArgs) (*types.ChapterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "chapter.add") {
return nil, ErrUnauthorized
}
story, err := story.FindID(input.StoryID)
if err != nil {
return nil, err
}
author := user.ID
if input.Author != nil {
author = *input.Author
if user.ID != author && !user.Permitted("chapter.add") {
return nil, ErrPermissionDenied
}
}
fictionalDate := time.Time{}
if input.FictionalDate != nil {
fictionalDate, err = time.Parse(time.RFC3339Nano, *input.FictionalDate)
if err != nil {
return nil, err
}
}
chapter, err := story.AddChapter(input.Title, author, input.Source, time.Now(), fictionalDate)
if err != nil {
return nil, err
}
go change.Submit("Chapter", "add", user.ID, chapter.ID, map[string]interface{}{
"title": input.Title,
"author": author,
"fictionalDate": fictionalDate,
})
return &types.ChapterResolver{C: chapter}, nil
}
@@ -0,0 +1,70 @@
package mutations
import (
"context"
"strings"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/character"
"git.aiterp.net/rpdata/api/model/log"
)
// AddCharacterInput is args for the addCharacter mutation
type AddCharacterInput struct {
Nick string
Name string
ShortName *string
Author *string
Description *string
}
// AddCharacter resolves the addCharacter mutation
func (r *MutationResolver) AddCharacter(ctx context.Context, args struct{ Input *AddCharacterInput }) (*types.CharacterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "character.add") {
return nil, ErrUnauthorized
}
nick := input.Nick
name := input.Name
shortName := ""
if input.ShortName != nil {
shortName = *input.ShortName
} else {
shortName = strings.SplitN(input.Name, " ", 2)[0]
}
author := user.ID
if input.Author != nil {
author = *input.Author
if author != user.ID && !user.Permitted("character.add") {
return nil, ErrPermissionDenied
}
}
description := ""
if input.Description != nil {
description = *input.Description
}
character, err := character.New(nick, name, shortName, author, description)
if err != nil {
return nil, err
}
go change.Submit("Character", "add", user.ID, character.ID, map[string]interface{}{
"name": character.Name,
"nick": character.Nicks[0],
"author": character.Author,
})
log.ScheduleCharacterUpdate()
return &types.CharacterResolver{C: character}, nil
}
@@ -0,0 +1,48 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/character"
"git.aiterp.net/rpdata/api/model/log"
)
// AddCharacterNickInput is args for the addCharacterNick mutation
type AddCharacterNickInput struct {
ID string
Nick string
}
// AddCharacterNick resolves the addCharacterNick mutation
func (r *MutationResolver) AddCharacterNick(ctx context.Context, args struct{ Input *AddCharacterNickInput }) (*types.CharacterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(input.ID)
if err != nil {
return nil, err
}
if character.Author != user.ID && !user.Permitted("character.edit") {
return nil, ErrPermissionDenied
}
err = character.AddNick(input.Nick)
if err != nil {
return nil, err
}
go change.Submit("Character", "add.nick", user.ID, character.ID, map[string]interface{}{
"nick": input.Nick,
})
log.ScheduleCharacterUpdate()
return &types.CharacterResolver{C: character}, nil
}
+70
View File
@@ -0,0 +1,70 @@
package mutations
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/log"
)
// LogAddArgs is args for the addLog mutation
type LogAddArgs struct {
Input *struct {
Date string
Channel string
Title *string
Open *bool
Event *string
Description *string
}
}
// AddLog resolves the addLog mutation
func (r *MutationResolver) AddLog(ctx context.Context, args *LogAddArgs) (*types.LogResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("log.add") {
return nil, ErrUnauthorized
}
date, err := time.Parse(time.RFC3339Nano, args.Input.Date)
if err != nil {
return nil, err
}
title := ""
if input.Title != nil {
title = *input.Title
}
event := ""
if input.Event != nil {
event = *input.Event
}
description := ""
if input.Description != nil {
description = *input.Description
}
open := input.Open != nil && *input.Open == true
log, err := log.New(date, input.Channel, title, event, description, open)
if err != nil {
return nil, err
}
go change.Submit("Log", "add", user.ID, log.ID, map[string]interface{}{
"channel": log.Channel,
"title": log.Title,
"event": log.Event,
"description": log.Description,
"open": log.Open,
})
return &types.LogResolver{L: log}, nil
}
+60
View File
@@ -0,0 +1,60 @@
package mutations
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/log"
)
// PostAddArgs is args for addPost mutation
type PostAddArgs struct {
Input *struct {
LogID string
Time string
Kind string
Nick string
Text string
}
}
// AddPost resolves the addPost mutation
func (r *MutationResolver) AddPost(ctx context.Context, args *PostAddArgs) (*types.PostResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.add") {
return nil, ErrUnauthorized
}
postTime, err := time.Parse(time.RFC3339Nano, input.Time)
if err != nil {
return nil, err
}
log, err := log.FindID(input.LogID)
if err != nil {
return nil, err
}
post, err := log.NewPost(postTime, input.Kind, input.Nick, input.Text)
if err != nil {
return nil, err
}
go change.Submit("Post", "add", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
"time": post.Time,
"kind": post.Kind,
"nick": post.Nick,
"text": post.Text,
"position": post.Position,
})
go log.UpdateCharacters()
return &types.PostResolver{P: post}, nil
}
+83
View File
@@ -0,0 +1,83 @@
package mutations
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// StoryAddArgs is args for the addStory mutation
type StoryAddArgs struct {
Input *struct {
Name string
Category string
Author *string
Open *bool
Listed *bool
FictionalDate *string
Tags *[]struct {
Kind string
Name string
}
}
}
// AddStory resolves the addStory mutation
func (r *MutationResolver) AddStory(ctx context.Context, args *StoryAddArgs) (*types.StoryResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.add") {
return nil, ErrUnauthorized
}
author := user.ID
if input.Author != nil {
author = *input.Author
if user.ID != author && !user.Permitted("story.add") {
return nil, ErrPermissionDenied
}
}
listed := (input.Listed != nil && *input.Listed == true)
open := (input.Open != nil && *input.Open == true)
tags := make([]story.Tag, 0, 8)
if input.Tags != nil {
for _, tagInput := range *input.Tags {
tags = append(tags, story.Tag{
Kind: tagInput.Kind,
Name: tagInput.Name,
})
}
}
fictionalDate := time.Time{}
if input.FictionalDate != nil {
date, err := time.Parse(time.RFC3339Nano, *input.FictionalDate)
if err != nil {
return nil, err
}
fictionalDate = date
}
story, err := story.New(input.Name, author, input.Category, listed, open, tags, time.Now(), fictionalDate)
if err != nil {
return nil, err
}
go change.Submit("Story", "add", user.ID, story.ID, map[string]interface{}{
"name": input.Name,
"category": input.Category,
"author": input.Author,
})
return &types.StoryResolver{S: story}, nil
}
+53
View File
@@ -0,0 +1,53 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// StoryTagAddArgs is args for the addStoryTag mutation
type StoryTagAddArgs struct {
Input *struct {
ID string
Tag struct {
Kind string
Name string
}
}
}
// AddStoryTag resolves the addStoryTag mutation
func (r *MutationResolver) AddStoryTag(ctx context.Context, args *StoryTagAddArgs) (*types.StoryResolver, error) {
input := args.Input
tag := story.Tag{Kind: input.Tag.Kind, Name: input.Tag.Name}
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.edit") {
return nil, ErrUnauthorized
}
story, err := story.FindID(input.ID)
if err != nil {
return nil, err
}
if story.Author != user.ID && !user.Permitted("story.edit") {
return nil, ErrPermissionDenied
}
err = story.AddTag(tag)
if err != nil {
return nil, err
}
go change.Submit("Story", "add.tag", user.ID, story.ID, map[string]interface{}{
"kind": tag.Kind,
"name": tag.Name,
})
return &types.StoryResolver{S: story}, nil
}
+50
View File
@@ -0,0 +1,50 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/channel"
)
// ChannelEditArgs is input for the editChannel mutation
type ChannelEditArgs struct {
Input *struct {
Name string
Logged *bool
Hub *bool
EventName *string
LocationName *string
}
}
// EditChannel resolves the editChannel mutation
func (r *MutationResolver) EditChannel(ctx context.Context, args *ChannelEditArgs) (*types.ChannelResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("channel.edit") {
return nil, ErrUnauthorized
}
channel, err := channel.FindName(input.Name)
if err != nil {
return nil, err
}
err = channel.Edit(input.Logged, input.Hub, input.EventName, input.LocationName)
if err != nil {
return nil, err
}
go change.Submit("Channel", "edit", user.ID, channel.Name, map[string]interface{}{
"logged": input.Logged,
"hub": input.Hub,
"location": input.LocationName,
"event": input.EventName,
})
return &types.ChannelResolver{C: channel}, nil
}
+63
View File
@@ -0,0 +1,63 @@
package mutations
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// EditChapterArgs is args for the editChapter mutation
type EditChapterArgs struct {
Input *struct {
ID string
Title *string
Source *string
FictionalDate *string
}
}
// EditChapter resolves the editChapter mutation
func (r *MutationResolver) EditChapter(ctx context.Context, args *EditChapterArgs) (*types.ChapterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "chapter.edit") {
return nil, ErrUnauthorized
}
chapter, err := story.FindChapterID(input.ID)
if err != nil {
return nil, err
}
if chapter.Author != user.ID && !user.Permitted("chapter.edit") {
return nil, ErrPermissionDenied
}
var fictionalDate *time.Time
if input.FictionalDate != nil {
date, err := time.Parse(time.RFC3339Nano, *input.FictionalDate)
if err != nil {
return nil, err
}
fictionalDate = &date
}
err = chapter.Edit(input.Title, input.Source, fictionalDate)
if err != nil {
return nil, err
}
go change.Submit("Chapter", "edit", user.ID, chapter.ID, map[string]interface{}{
"title": input.Title,
"source": input.Source,
"fictionalDate": fictionalDate,
})
return &types.ChapterResolver{C: chapter}, nil
}
@@ -0,0 +1,64 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/character"
)
// CharacterEditInput is args for mutation addCharacterNick/removeCharacterNick
type CharacterEditInput struct {
ID string
Name *string
ShortName *string
Description *string
}
// EditCharacter resolves the editCharacter mutation
func (r *MutationResolver) EditCharacter(ctx context.Context, args struct{ Input *CharacterEditInput }) (*types.CharacterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(input.ID)
if err != nil {
return nil, err
}
if character.Author != user.ID && !user.Permitted("character.edit") {
return nil, ErrPermissionDenied
}
name := ""
if input.Name != nil {
name = *input.Name
}
shortName := ""
if input.ShortName != nil {
shortName = *input.ShortName
}
description := ""
if input.Description != nil {
description = *input.Description
}
err = character.Edit(name, shortName, description)
if err != nil {
return nil, err
}
go change.Submit("Character", "edit", user.ID, character.ID, map[string]interface{}{
"name": character.Name,
"shortName": character.ShortName,
"description": character.Description,
})
return &types.CharacterResolver{C: character}, nil
}
+43
View File
@@ -0,0 +1,43 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/file"
)
// FileEditArgs is args for the editFile mutation
type FileEditArgs struct {
Input *struct {
ID string
Name *string
Public *bool
}
}
// EditFile resolves the editFile mutation
func (r *MutationResolver) EditFile(ctx context.Context, args *FileEditArgs) (*types.FileResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
file, err := file.FindID(input.ID)
if err != nil {
return nil, err
}
if file.Author != user.ID && !user.Permitted("file.edit") {
return nil, ErrUnauthorized
}
err = file.Edit(input.Name, input.Public)
if err != nil {
return nil, err
}
return &types.FileResolver{F: file}, nil
}
+51
View File
@@ -0,0 +1,51 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/log"
)
// LogEditArgs is args for the editLog mutation
type LogEditArgs struct {
Input *struct {
ID string
Title *string
Event *string
Description *string
Open *bool
}
}
// EditLog resolves the editLog mutation
func (r *MutationResolver) EditLog(ctx context.Context, args *LogEditArgs) (*types.LogResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("log.edit") {
return nil, ErrUnauthorized
}
log, err := log.FindID(input.ID)
if err != nil {
return nil, err
}
err = log.Edit(input.Title, input.Event, input.Description, input.Open)
if err != nil {
return nil, err
}
go change.Submit("Log", "edit", user.ID, log.ID, map[string]interface{}{
"channel": log.Channel,
"title": input.Title,
"event": input.Event,
"description": input.Description,
"open": input.Open,
})
return &types.LogResolver{L: log}, nil
}
+63
View File
@@ -0,0 +1,63 @@
package mutations
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/log"
)
// PostEditArgs is args for the editPost mutation
type PostEditArgs struct {
Input *struct {
ID string
Time *string
Kind *string
Nick *string
Text *string
}
}
// EditPost resolves the editPost mutation
func (r *MutationResolver) EditPost(ctx context.Context, args *PostEditArgs) (*types.PostResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.edit") {
return nil, ErrUnauthorized
}
postTime := (*time.Time)(nil)
if args.Input.Time != nil {
t, err := time.Parse(time.RFC3339Nano, *input.Time)
if err != nil {
return nil, err
}
postTime = &t
}
post, err := log.FindPostID(input.ID)
if err != nil {
return nil, err
}
err = post.Edit(postTime, input.Kind, input.Nick, input.Text)
if err != nil {
return nil, err
}
go change.Submit("Post", "edit", user.ID, post.ID, map[string]interface{}{
"time": postTime,
"kind": input.Kind,
"nick": input.Nick,
"text": input.Text,
})
go log.UpdateCharacters(post.LogID)
return &types.PostResolver{P: post}, nil
}
+69
View File
@@ -0,0 +1,69 @@
package mutations
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// StoryEditArgs is args for the addStory mutation
type StoryEditArgs struct {
Input *struct {
ID string
Name *string
Category *string
Author *string
Open *bool
Listed *bool
FictionalDate *string
}
}
// EditStory resolves the editStory mutation
func (r *MutationResolver) EditStory(ctx context.Context, args *StoryEditArgs) (*types.StoryResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.edit") {
return nil, ErrUnauthorized
}
story, err := story.FindID(input.ID)
if err != nil {
return nil, err
}
if story.Author != user.ID && !user.Permitted("story.edit") {
return nil, ErrPermissionDenied
}
var fictionalDate *time.Time
if input.FictionalDate != nil {
date, err := time.Parse(time.RFC3339Nano, *input.FictionalDate)
if err != nil {
return nil, err
}
fictionalDate = &date
}
err = story.Edit(input.Name, input.Category, input.Listed, input.Open, fictionalDate)
if err != nil {
return nil, err
}
go change.Submit("Story", "edit", user.ID, story.ID, map[string]interface{}{
"name": input.Name,
"category": input.Category,
"author": input.Author,
"open": input.Open,
"listed": input.Listed,
"fictionalDate": input.FictionalDate,
})
return &types.StoryResolver{S: story}, nil
}
+26
View File
@@ -0,0 +1,26 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
)
// LoginArgs is args for the login mutation
type LoginArgs struct {
Username string
Password string
}
// Login resolves the login mutation
func (r *MutationResolver) Login(ctx context.Context, args *LoginArgs) (*types.SessionResolver, error) {
session := session.FromContext(ctx)
err := session.Login(args.Username, args.Password)
if err != nil {
return nil, err
}
return &types.SessionResolver{S: session}, nil
}
+16
View File
@@ -0,0 +1,16 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
)
// Logout resolves mutation.logout
func (r *MutationResolver) Logout(ctx context.Context) (*types.SessionResolver, error) {
session := session.FromContext(ctx)
session.Logout()
return &types.SessionResolver{S: session}, nil
}
+45
View File
@@ -0,0 +1,45 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/log"
)
// PostMoveArgs is args for movePost mutation
type PostMoveArgs struct {
Input *struct {
ID string
ToPosition int32
}
}
// MovePost resolves the movePost mutation
func (r *MutationResolver) MovePost(ctx context.Context, args *PostMoveArgs) (*types.PostResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.move") {
return nil, ErrUnauthorized
}
post, err := log.FindPostID(input.ID)
if err != nil {
return nil, err
}
err = post.Move(int(input.ToPosition))
if err != nil {
return nil, err
}
go change.Submit("Post", "move", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
"targetIndex": input.ToPosition,
})
return &types.PostResolver{P: post}, nil
}
@@ -0,0 +1,37 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/channel"
)
// RemoveChannelArgs is args for removeChannels mutation
type RemoveChannelArgs struct {
Name string
}
// RemoveChannel resolves the editChannel mutation
func (r *MutationResolver) RemoveChannel(ctx context.Context, args RemoveChannelArgs) (*types.ChannelResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("channel.remove") {
return nil, ErrUnauthorized
}
channel, err := channel.FindName(args.Name)
if err != nil {
return nil, err
}
err = channel.Remove()
if err != nil {
return nil, err
}
go change.Submit("Channel", "remove", user.ID, channel.Name, nil)
return &types.ChannelResolver{C: channel}, nil
}
@@ -0,0 +1,37 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// RemoveChapterArgs is args for the addChapter mutation
type RemoveChapterArgs struct {
ID string
}
// RemoveChapter resolves the removeChapter mutation
func (r *MutationResolver) RemoveChapter(ctx context.Context, args *RemoveChapterArgs) (*types.ChapterResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "chapter.edit") {
return nil, ErrUnauthorized
}
chapter, err := story.FindChapterID(args.ID)
if err != nil {
return nil, err
}
err = chapter.Remove()
if err != nil {
return nil, err
}
go change.Submit("Chapter", "remove", user.ID, chapter.ID, nil)
return &types.ChapterResolver{C: chapter}, nil
}
@@ -0,0 +1,44 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/character"
)
// RemoveCharacterArgs is args for the removeCharacter mutation
type RemoveCharacterArgs struct {
ID string
}
// RemoveCharacter resolves the removeCharacter mutation
func (r *MutationResolver) RemoveCharacter(ctx context.Context, args RemoveCharacterArgs) (*types.CharacterResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(args.ID)
if err != nil {
return nil, err
}
if character.Author != user.ID && !user.Permitted("character.remove") {
return nil, ErrPermissionDenied
}
err = character.Remove()
if err != nil {
return nil, err
}
go change.Submit("Character", "remove", user.ID, character.ID, map[string]interface{}{
"name": character.Name,
"author": character.Author,
"nicks": character.Nicks,
})
return &types.CharacterResolver{C: character}, nil
}
@@ -0,0 +1,48 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/character"
"git.aiterp.net/rpdata/api/model/log"
)
// RemoveCharacterNick is args for the addCharacterNick mutation
type RemoveCharacterNick struct {
ID string
Nick string
}
// RemoveCharacterNick resolves the removeCharacterNick mutation
func (r *MutationResolver) RemoveCharacterNick(ctx context.Context, args struct{ Input *RemoveCharacterNick }) (*types.CharacterResolver, error) {
input := args.Input
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(input.ID)
if err != nil {
return nil, err
}
if character.Author != user.ID && !user.Permitted("character.edit") {
return nil, ErrPermissionDenied
}
err = character.RemoveNick(input.Nick)
if err != nil {
return nil, err
}
go change.Submit("Character", "remove.nick", user.ID, character.ID, map[string]interface{}{
"nick": input.Nick,
})
log.ScheduleCharacterUpdate()
return &types.CharacterResolver{C: character}, nil
}
+37
View File
@@ -0,0 +1,37 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/file"
)
// RemoveFileArgs is an arg
type RemoveFileArgs struct {
ID string
}
// RemoveFile resolves the removeFIle mutation
func (r *MutationResolver) RemoveFile(ctx context.Context, args *RemoveFileArgs) (*types.FileResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
file, err := file.FindID(args.ID)
if err != nil {
return nil, err
}
if file.Author != user.ID && !user.Permitted("file.remove") {
return nil, ErrUnauthorized
}
err = file.Delete()
if err != nil {
return nil, err
}
return &types.FileResolver{F: file}, nil
}
+37
View File
@@ -0,0 +1,37 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/log"
)
// RemoveLogArgs is args for the removeLog mutation
type RemoveLogArgs struct {
ID string
}
// RemoveLog resolves the removeLog mutation
func (r *MutationResolver) RemoveLog(ctx context.Context, args *RemoveLogArgs) (*types.LogResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("log.remove") {
return nil, ErrUnauthorized
}
log, err := log.FindID(args.ID)
if err != nil {
return nil, err
}
err = log.Remove()
if err != nil {
return nil, err
}
go change.Submit("Log", "remove", user.ID, log.ID, nil)
return &types.LogResolver{L: log}, nil
}
+36
View File
@@ -0,0 +1,36 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/log"
)
// PostRemoveArgs is args for the removePost mutation
type PostRemoveArgs struct {
ID string
}
// RemovePost resolves the removePost mutation
func (r *MutationResolver) RemovePost(ctx context.Context, args PostRemoveArgs) (*types.PostResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.remove") {
return nil, ErrUnauthorized
}
post, err := log.RemovePost(args.ID)
if err != nil {
return nil, err
}
go change.Submit("Post", "remove", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
})
go log.UpdateCharacters(post.LogID)
return &types.PostResolver{P: post}, nil
}
+41
View File
@@ -0,0 +1,41 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// StoryRemoveArgs is args for the removeStory mutation
type StoryRemoveArgs struct {
ID string
}
// RemoveStory resolves the removeStory mutation
func (r *MutationResolver) RemoveStory(ctx context.Context, args *StoryRemoveArgs) (*types.StoryResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.edit") {
return nil, ErrUnauthorized
}
story, err := story.FindID(args.ID)
if err != nil {
return nil, err
}
if story.Author != user.ID && !user.Permitted("story.remove") {
return nil, ErrPermissionDenied
}
err = story.Remove()
if err != nil {
return nil, err
}
go change.Submit("Story", "remove", user.ID, story.ID, nil)
return &types.StoryResolver{S: story}, nil
}
@@ -0,0 +1,53 @@
package mutations
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/story"
)
// StoryTagRemoveArgs is args for the removeStoryTag mutation
type StoryTagRemoveArgs struct {
Input *struct {
ID string
Tag struct {
Kind string
Name string
}
}
}
// RemoveStoryTag resolves the removeStoryTag mutation
func (r *MutationResolver) RemoveStoryTag(ctx context.Context, args *StoryTagRemoveArgs) (*types.StoryResolver, error) {
input := args.Input
tag := story.Tag{Kind: input.Tag.Kind, Name: input.Tag.Name}
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "story.edit") {
return nil, ErrUnauthorized
}
story, err := story.FindID(input.ID)
if err != nil {
return nil, err
}
if story.Author != user.ID && !user.Permitted("story.edit") {
return nil, ErrPermissionDenied
}
err = story.RemoveTag(tag)
if err != nil {
return nil, err
}
go change.Submit("Story", "remove.tag", user.ID, story.ID, map[string]interface{}{
"kind": tag.Kind,
"name": tag.Name,
})
return &types.StoryResolver{S: story}, nil
}
+14
View File
@@ -0,0 +1,14 @@
// Package mutations contains resolvers for individual GrahpQL mutations. They were previously mixed in with one another, but that caused
// difficulty seeking out specific mutations.
package mutations
import "errors"
// MutationResolver is a resolver for all mutations. The stuttery name is due to being embedded in the root resolver.
type MutationResolver struct{}
// ErrUnauthorized is when a guest acts like they own the place
var ErrUnauthorized = errors.New("Unauthorized")
// ErrPermissionDenied is returned when users act above their station
var ErrPermissionDenied = errors.New("Permission denied")
+23
View File
@@ -0,0 +1,23 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/channel"
)
// ChannelArgs is args for channel query
type ChannelArgs struct {
Name string
}
// Channel resolves the channel query
func (r *QueryResolver) Channel(ctx context.Context, args *ChannelArgs) (*types.ChannelResolver, error) {
channel, err := channel.FindName(args.Name)
if err != nil {
return nil, err
}
return &types.ChannelResolver{C: channel}, nil
}
+43
View File
@@ -0,0 +1,43 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/channel"
)
// ChannelsArgs is args for channel query
type ChannelsArgs struct {
Filter *struct {
Logged *bool
EventName *string
LocationName *string
}
}
// Channels resolves the channels query
func (r *QueryResolver) Channels(ctx context.Context, args *ChannelsArgs) ([]*types.ChannelResolver, error) {
var channels []channel.Channel
var err error
filter := args.Filter
if filter != nil {
channels, err = channel.List(filter.Logged, filter.EventName, filter.LocationName)
if err != nil {
return nil, err
}
} else {
channels, err = channel.List(nil, nil, nil)
if err != nil {
return nil, err
}
}
resolvers := make([]*types.ChannelResolver, len(channels))
for i := range channels {
resolvers[i] = &types.ChannelResolver{C: channels[i]}
}
return resolvers, nil
}
+23
View File
@@ -0,0 +1,23 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/story"
)
// ChapterArgs is args for chapter query
type ChapterArgs struct {
ID string
}
// Chapter resolves the chapter query
func (r *QueryResolver) Chapter(ctx context.Context, args *ChapterArgs) (*types.ChapterResolver, error) {
chapter, err := story.FindChapterID(args.ID)
if err != nil {
return nil, err
}
return &types.ChapterResolver{C: chapter}, nil
}
+43
View File
@@ -0,0 +1,43 @@
package queries
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/character"
)
// ErrIncorrectArguments is returned by character query.
var ErrIncorrectArguments = errors.New("You can only query character by ID or Nick")
// CharacterArgs is args for character query
type CharacterArgs struct {
ID *string
Nick *string
}
// Character resolves the character query
func (r *QueryResolver) Character(ctx context.Context, args *CharacterArgs) (*types.CharacterResolver, error) {
var char character.Character
var err error
if args.Nick != nil && args.ID != nil {
return nil, ErrIncorrectArguments
}
switch {
case args.ID != nil:
char, err = character.FindID(*args.ID)
case args.Nick != nil:
char, err = character.FindNick(*args.Nick)
default:
err = ErrIncorrectArguments
}
if err != nil {
return nil, err
}
return &types.CharacterResolver{C: char}, nil
}
+59
View File
@@ -0,0 +1,59 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/character"
)
// CharactersArgs is args for characters query
type CharactersArgs struct {
Filter *struct {
IDs *[]string
Nicks *[]string
Names *[]string
Author *string
Search *string
}
}
// Characters resolves the characters query
func (r *QueryResolver) Characters(ctx context.Context, args *CharactersArgs) ([]*types.CharacterResolver, error) {
var characters []character.Character
var err error
filter := args.Filter
if filter != nil {
var ids []string
var nicks []string
var names []string
if filter.IDs != nil {
ids = *filter.IDs
}
if filter.Nicks != nil {
nicks = *filter.Nicks
}
if filter.Names != nil {
names = *filter.Names
}
characters, err = character.ListFilter(ids, nicks, names, filter.Author, filter.Search)
if err != nil {
return nil, err
}
} else {
characters, err = character.List()
if err != nil {
return nil, err
}
}
resolvers := make([]*types.CharacterResolver, 0, len(characters))
for _, character := range characters {
resolvers = append(resolvers, &types.CharacterResolver{C: character})
}
return resolvers, nil
}
+23
View File
@@ -0,0 +1,23 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/file"
)
// FileArgs is args for file query
type FileArgs struct {
ID string
}
// File resolves the file query
func (r *QueryResolver) File(ctx context.Context, args *FileArgs) (*types.FileResolver, error) {
file, err := file.FindID(args.ID)
if err != nil {
return nil, err
}
return &types.FileResolver{F: file}, nil
}
+45
View File
@@ -0,0 +1,45 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/file"
)
// FilesArgs is args for files query
type FilesArgs struct {
Filter *struct {
Public *bool
MimeTypes *[]string
}
}
// Files resolves the file query
func (r *QueryResolver) Files(ctx context.Context, args *FilesArgs) ([]*types.FileResolver, error) {
filter := args.Filter
user := session.FromContext(ctx).User()
author := ""
if user != nil {
author = user.ID
}
mimeTypes := []string(nil)
if filter.MimeTypes != nil {
mimeTypes = *filter.MimeTypes
}
files, err := file.List(author, filter.Public, mimeTypes)
if err != nil {
return nil, err
}
resolvers := make([]*types.FileResolver, len(files))
for i := range files {
resolvers[i] = &types.FileResolver{F: files[i]}
}
return resolvers, nil
}
+23
View File
@@ -0,0 +1,23 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/log"
)
// LogArgs is args for log query
type LogArgs struct {
ID string
}
// Log resolves the log query
func (r *QueryResolver) Log(ctx context.Context, args *LogArgs) (*types.LogResolver, error) {
log, err := log.FindID(args.ID)
if err != nil {
return nil, err
}
return &types.LogResolver{L: log}, nil
}
+79
View File
@@ -0,0 +1,79 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/log"
)
// LogsArgs is args for the logs query
type LogsArgs struct {
Filter *struct {
Search *string
Characters *[]string
Channels *[]string
Events *[]string
Open *bool
Limit *int32
}
}
// Logs resolves the logs query
func (r *QueryResolver) Logs(ctx context.Context, args *LogsArgs) ([]*types.LogResolver, error) {
var logs []log.Log
var err error
filter := args.Filter
if filter != nil {
limit := 100
search := ""
if filter.Search != nil {
search = *filter.Search
limit = 0
}
channels := []string(nil)
if filter.Channels != nil {
channels = *filter.Channels
limit = 0
}
characters := []string(nil)
if filter.Characters != nil {
characters = *filter.Characters
limit = 0
}
events := []string(nil)
if filter.Events != nil {
events = *filter.Events
limit = 0
}
if filter.Limit != nil {
limit = int(*filter.Limit)
}
open := filter.Open != nil && *filter.Open == true
logs, err = log.ListSearch(search, channels, characters, events, open, limit)
if err != nil {
return nil, err
}
} else {
logs, err = log.List(100)
if err != nil {
return nil, err
}
}
resolvers := make([]*types.LogResolver, len(logs))
for i := range logs {
resolvers[i] = &types.LogResolver{L: logs[i]}
}
return resolvers, nil
}
+23
View File
@@ -0,0 +1,23 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/log"
)
// PostArgs is args for the post query
type PostArgs struct {
ID string
}
// Post resolves the post query
func (r *QueryResolver) Post(ctx context.Context, args *PostArgs) (*types.PostResolver, error) {
post, err := log.FindPostID(args.ID)
if err != nil {
return nil, err
}
return &types.PostResolver{P: post}, nil
}
+28
View File
@@ -0,0 +1,28 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/log"
)
// PostsArgs is args for the posts query
type PostsArgs struct {
IDs []string
}
// Posts resolves the posts query
func (r *QueryResolver) Posts(ctx context.Context, args *PostsArgs) ([]*types.PostResolver, error) {
posts, err := log.ListPostIDs(args.IDs...)
if err != nil {
return nil, err
}
resolvers := make([]*types.PostResolver, len(posts))
for i := range resolvers {
resolvers[i] = &types.PostResolver{P: posts[i]}
}
return resolvers, nil
}
+11
View File
@@ -0,0 +1,11 @@
// Package queries contains resolvers for individual GrahpQL queries. They were previously mixed in with one another, but that caused
// difficulty seeking out specific queries.
package queries
import "errors"
// QueryResolver is a resolver for all queries. The stuttery name is due to being embedded in the root resolver.
type QueryResolver struct{}
// ErrCannotResolve is returned when a resolver constructor is at its wit's end
var ErrCannotResolve = errors.New("Cannot resolve due to invalid arguments")
+13
View File
@@ -0,0 +1,13 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/internal/session"
)
// Session resolves query.session
func (r *QueryResolver) Session(ctx context.Context) (*types.SessionResolver, error) {
return &types.SessionResolver{S: session.FromContext(ctx)}, nil
}
+77
View File
@@ -0,0 +1,77 @@
package queries
import (
"context"
"time"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/story"
)
// StoriesArg is args for stories query
type StoriesArg struct {
Filter *struct {
Author *string
Tags *[]struct {
Kind string
Name string
}
EarliestFictionalDate *string
LatestFictionalDate *string
Limit *int32
}
}
// Stories resolves the stories query
func (r *QueryResolver) Stories(ctx context.Context, args *StoriesArg) ([]*types.StoryResolver, error) {
filter := args.Filter
author := ""
if filter != nil && filter.Author != nil {
author = *filter.Author
}
tags := make([]story.Tag, 0, 8)
if filter != nil && filter.Tags != nil {
for _, tagInput := range *filter.Tags {
tags = append(tags, story.Tag{
Kind: tagInput.Kind,
Name: tagInput.Name,
})
}
}
earliest := time.Time{}
err := error(nil)
if filter != nil && filter.EarliestFictionalDate != nil {
earliest, err = time.Parse(time.RFC3339Nano, *filter.EarliestFictionalDate)
if err != nil {
return nil, err
}
}
latest := time.Time{}
if filter != nil && filter.LatestFictionalDate != nil {
latest, err = time.Parse(time.RFC3339Nano, *filter.LatestFictionalDate)
if err != nil {
return nil, err
}
}
limit := 30
if filter != nil && filter.Limit != nil {
limit = int(*filter.Limit)
}
stories, err := story.List(author, tags, earliest, latest, limit)
if err != nil {
return nil, err
}
resolvers := make([]*types.StoryResolver, len(stories))
for i, story := range stories {
resolvers[i] = &types.StoryResolver{S: story}
}
return resolvers, nil
}
+23
View File
@@ -0,0 +1,23 @@
package queries
import (
"context"
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/story"
)
// StoryArgs is args for story query
type StoryArgs struct {
ID string
}
// Story resolves the story query
func (r *QueryResolver) Story(ctx context.Context, args *StoryArgs) (*types.StoryResolver, error) {
story, err := story.FindID(args.ID)
if err != nil {
return nil, err
}
return &types.StoryResolver{S: story}, nil
}
+21
View File
@@ -0,0 +1,21 @@
package queries
import (
"git.aiterp.net/rpdata/api/graphql/resolver/types"
"git.aiterp.net/rpdata/api/model/story"
)
// Tags resolves the tags query
func (r *QueryResolver) Tags() ([]*types.TagResolver, error) {
tags, err := story.ListTags()
if err != nil {
return nil, err
}
resolvers := make([]*types.TagResolver, len(tags))
for i, tag := range tags {
resolvers[i] = &types.TagResolver{T: tag}
}
return resolvers, nil
}
+13
View File
@@ -0,0 +1,13 @@
package resolver
import (
"git.aiterp.net/rpdata/api/graphql/resolver/mutations"
"git.aiterp.net/rpdata/api/graphql/resolver/queries"
)
// The Resolver combines the query and mutation resolvers from the subpackages.
// This is the one to pass along with the schema.
type Resolver struct {
queries.QueryResolver
mutations.MutationResolver
}
+39
View File
@@ -0,0 +1,39 @@
package types
import "git.aiterp.net/rpdata/api/model/channel"
// ChannelResolver for the Channel graphql type
type ChannelResolver struct{ C channel.Channel }
// Name resolves channel.name
func (r *ChannelResolver) Name() string {
return r.C.Name
}
// Logged resolves channel.logged
func (r *ChannelResolver) Logged() bool {
return r.C.Logged
}
// Hub resolves channel.hub
func (r *ChannelResolver) Hub() bool {
return r.C.Hub
}
// EventName resolves channel.eventName
func (r *ChannelResolver) EventName() *string {
if r.C.Event == "" {
return nil
}
return &r.C.Event
}
// LocationName resolves channel.locationName
func (r *ChannelResolver) LocationName() *string {
if r.C.Location == "" {
return nil
}
return &r.C.Location
}
+50
View File
@@ -0,0 +1,50 @@
package types
import (
"time"
"git.aiterp.net/rpdata/api/model/story"
)
// ChapterResolver for the Chapter graphql type
type ChapterResolver struct{ C story.Chapter }
// ID resolves Chapter.id
func (r *ChapterResolver) ID() string {
return r.C.ID
}
// StoryID resolves Chapter.storyId
func (r *ChapterResolver) StoryID() string {
return r.C.StoryID
}
// Title resolves Chapter.title
func (r *ChapterResolver) Title() string {
return r.C.Title
}
// Author resolves Chapter.author
func (r *ChapterResolver) Author() string {
return r.C.Author
}
// Source resolves Chapter.source
func (r *ChapterResolver) Source() string {
return r.C.Source
}
// CreatedDate resolves Chapter.createdDate
func (r *ChapterResolver) CreatedDate() string {
return r.C.CreatedDate.Format(time.RFC3339Nano)
}
// FictionalDate resolves Chapter.fictionalDate
func (r *ChapterResolver) FictionalDate() string {
return r.C.FictionalDate.Format(time.RFC3339Nano)
}
// EditedDate resolves Chapter.editedDate
func (r *ChapterResolver) EditedDate() string {
return r.C.EditedDate.Format(time.RFC3339Nano)
}
+45
View File
@@ -0,0 +1,45 @@
package types
import "git.aiterp.net/rpdata/api/model/character"
// CharacterResolver for the Character graphql type
type CharacterResolver struct{ C character.Character }
// ID is a property resolver
func (r *CharacterResolver) ID() string {
return r.C.ID
}
// Nick is a property resolver
func (r *CharacterResolver) Nick() *string {
if len(r.C.Nicks) == 0 {
return nil
}
return &r.C.Nicks[0]
}
// Nicks is a property resolver
func (r *CharacterResolver) Nicks() []string {
return r.C.Nicks
}
// Name is a property resolver
func (r *CharacterResolver) Name() string {
return r.C.Name
}
// ShortName is a property resolver
func (r *CharacterResolver) ShortName() string {
return r.C.ShortName
}
// Author is a property resolver
func (r *CharacterResolver) Author() string {
return r.C.Author
}
// Description is a property resolver
func (r *CharacterResolver) Description() string {
return r.C.Description
}
+59
View File
@@ -0,0 +1,59 @@
package types
import (
"time"
"git.aiterp.net/rpdata/api/model/file"
)
// FileResolver for the File graphql type
type FileResolver struct{ F file.File }
// ID resolves File.id
func (f *FileResolver) ID() string {
return f.F.ID
}
// Author resolves File.author
func (f *FileResolver) Author() string {
return f.F.Author
}
// Kind resolves File.kind
func (f *FileResolver) Kind() string {
return f.F.Kind
}
// Time resolves File.time
func (f *FileResolver) Time() string {
return f.F.Time.Format(time.RFC3339Nano)
}
// Public resolves File.public
func (f *FileResolver) Public() bool {
return f.F.Public
}
// Name resolves File.name
func (f *FileResolver) Name() string {
return f.F.Name
}
// MimeType resolves File.mimeType
func (f *FileResolver) MimeType() string {
return f.F.MimeType
}
// Size resolves File.size
func (f *FileResolver) Size() int32 {
return int32(f.F.Size)
}
// URL resolves File.url
func (f *FileResolver) URL() *string {
if f.F.URL == "" {
return nil
}
return &f.F.URL
}
+117
View File
@@ -0,0 +1,117 @@
package types
import (
"context"
"errors"
"time"
"git.aiterp.net/rpdata/api/graphql/loader"
"git.aiterp.net/rpdata/api/model/log"
)
// LogResolver for the Log graphql type
type LogResolver struct{ L log.Log }
// ID resolves Log.id
func (r *LogResolver) ID() string {
return r.L.ID
}
// ShortID resolves Log.shortId
func (r *LogResolver) ShortID() string {
return r.L.ShortID
}
// Date resolves Log.date
func (r *LogResolver) Date() string {
return r.L.Date.Format(time.RFC3339Nano)
}
// ChannelName resolves Log.channelName
func (r *LogResolver) ChannelName() string {
return r.L.Channel
}
// Channel resolves Log.channe
func (r *LogResolver) Channel(ctx context.Context) (*ChannelResolver, error) {
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
channel, err := loader.Channel("name", r.L.Channel)
if err != nil {
return nil, err
}
return &ChannelResolver{C: channel}, nil
}
// Title resolves Log.title
func (r *LogResolver) Title() string {
return r.L.Title
}
// Event resolves Log.event
func (r *LogResolver) Event() string {
return r.L.Event
}
// Description resolves Log.description
func (r *LogResolver) Description() string {
return r.L.Description
}
// Open resolves Log.open
func (r *LogResolver) Open() bool {
return r.L.Open
}
// Characters resolves Log.characters
func (r *LogResolver) Characters(ctx context.Context) ([]*CharacterResolver, error) {
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
chars, err := loader.Characters("id", r.L.CharacterIDs...)
if err != nil {
return nil, err
}
resolvers := make([]*CharacterResolver, 0, len(chars))
for i := range chars {
if chars[i].ID == "" {
continue
}
resolvers = append(resolvers, &CharacterResolver{C: chars[i]})
}
return resolvers, nil
}
// Posts resolves Log.posts
func (r *LogResolver) Posts(ctx context.Context, args *LogPostArgs) ([]*PostResolver, error) {
var kinds []string
if args.Kinds != nil {
kinds = *args.Kinds
}
posts, err := r.L.Posts(kinds...)
if err != nil {
return nil, err
}
resolvers := make([]*PostResolver, len(posts))
for i := range posts {
resolvers[i] = &PostResolver{posts[i]}
}
return resolvers, nil
}
// LogPostArgs is an arg
type LogPostArgs struct {
Kinds *[]string
}
+45
View File
@@ -0,0 +1,45 @@
package types
import (
"time"
"git.aiterp.net/rpdata/api/model/log"
)
// PostResolver for the Post graphql type
type PostResolver struct{ P log.Post }
// ID resolves Post.id
func (r *PostResolver) ID() string {
return r.P.ID
}
// LogID resolves Post.logId
func (r *PostResolver) LogID() string {
return r.P.LogID
}
// Time resolves Post.time
func (r *PostResolver) Time() string {
return r.P.Time.Format(time.RFC3339Nano)
}
// Kind resolves Post.logId
func (r *PostResolver) Kind() string {
return r.P.Kind
}
// Nick resolves Post.nick
func (r *PostResolver) Nick() string {
return r.P.Nick
}
// Text resolves Post.text
func (r *PostResolver) Text() string {
return r.P.Text
}
// Position resolves Post.text
func (r *PostResolver) Position() int32 {
return int32(r.P.Position)
}
+16
View File
@@ -0,0 +1,16 @@
package types
import "git.aiterp.net/rpdata/api/internal/session"
// SessionResolver resolves Session
type SessionResolver struct{ S *session.Session }
// User resolves Session.user
func (r *SessionResolver) User() *UserResolver {
user := r.S.User()
if user == nil {
return nil
}
return &UserResolver{U: user}
}
+80
View File
@@ -0,0 +1,80 @@
package types
import (
"time"
"git.aiterp.net/rpdata/api/model/story"
)
// StoryResolver for the Story graphql type
type StoryResolver struct{ S story.Story }
// ID resolves Story.id
func (r *StoryResolver) ID() string {
return r.S.ID
}
// Author resolves Story.author
func (r *StoryResolver) Author() string {
return r.S.Author
}
// Name resolves Story.name
func (r *StoryResolver) Name() string {
return r.S.Name
}
// Category resolves Story.category
func (r *StoryResolver) Category() string {
return r.S.Category
}
// Open resolves Story.open
func (r *StoryResolver) Open() bool {
return r.S.Open
}
// Listed resolves Story.listed
func (r *StoryResolver) Listed() bool {
return r.S.Listed
}
// Tags resolves Story.tags
func (r *StoryResolver) Tags() []*TagResolver {
resolvers := make([]*TagResolver, len(r.S.Tags))
for i, tag := range r.S.Tags {
resolvers[i] = &TagResolver{T: tag}
}
return resolvers
}
// Chapters resolves Story.chapters
func (r *StoryResolver) Chapters() ([]*ChapterResolver, error) {
chapters, err := r.S.Chapters()
if err != nil {
return nil, err
}
resolvers := make([]*ChapterResolver, len(chapters))
for i, chapter := range chapters {
resolvers[i] = &ChapterResolver{C: chapter}
}
return resolvers, nil
}
// CreatedDate resolves Story.createdDate
func (r *StoryResolver) CreatedDate() string {
return r.S.CreatedDate.Format(time.RFC3339Nano)
}
// FictionalDate resolves Story.fictionalDate
func (r *StoryResolver) FictionalDate() string {
return r.S.FictionalDate.Format(time.RFC3339Nano)
}
// UpdatedDate resolves Story.updatedDate
func (r *StoryResolver) UpdatedDate() string {
return r.S.UpdatedDate.Format(time.RFC3339Nano)
}
+16
View File
@@ -0,0 +1,16 @@
package types
import "git.aiterp.net/rpdata/api/model/story"
// TagResolver for the Tag graphql type
type TagResolver struct{ T story.Tag }
// Kind resolves Tag.kind
func (r *TagResolver) Kind() string {
return r.T.Kind
}
// Name resolves Tag.name
func (r *TagResolver) Name() string {
return r.T.Name
}
@@ -1,4 +1,4 @@
package resolver
package types
import "git.aiterp.net/rpdata/api/internal/session"
@@ -3,37 +3,52 @@ type Query {
# Find character by either an ID or a nick.
character(id: String, nick: String): Character
# Find characters by either a list of ids, nicks or an author. Only one parameter at a time
characters(ids: [String!], nicks: [String!], author: String): [Character!]!
# Find characters
characters(filter: CharactersFilter): [Character!]!
# Find channel by name
channel(name: String!): Channel
# Find all channels. Specifying `logged: true` restricts the search to only logged.
channels(logged: Boolean): [Channel!]!
# Find channels
channels(filter: ChannelsFilter): [Channel!]!
# Find log by ID
log(id: String): Log
log(id: String!): Log
# Find logs by a list of IDs
logs(input: LogQueryInput): [LogHeader!]!
# Find logs
logs(filter: LogsFilter): [LogHeader!]!
# Find post by ID
# Find post by ID.
post(id: String!): Post
# Find posts by IDs. It's meant to allow other parts of the UI to link to a cluster of posts, e.g. for a room description for the
# Mapp should it ever become a thing.
# Mapp should it ever become a thing. This does not have a filter, since it's meant to be queried in the logs' response's selection set.
posts(ids: [String!]!): [Post!]!
# Find file by ID
file(id: String!): File
# Files get all available files. If includePublic is set, it will return public files as well
files(includePublic: Boolean, mimeTypes: [String!]): [File!]!
# Find files
files(filter: FilesFilter): [File!]!
# Find story by ID
story(id: String!): Story
# Find stories
stories(filter: StoriesFilter): [Story!]!
# Find story chapter by ID
chapter(id: String!): Chapter
# Find all distinct tags used in stories
tags: [Tag!]!
# Find current session
@@ -95,7 +110,33 @@ type Mutation {
editFile(input: EditFileInput!): File!
# Remove a file
removeFile(id: String!): File
removeFile(id: String!): File!
# Add a story
addStory(input: StoryAddInput!): Story!
# Add a story tag
addStoryTag(input: StoryTagAddInput!): Story!
# Remove a story tag
removeStoryTag(input: StoryTagRemoveInput!): Story!
# Edit a story
editStory(input: StoryEditInput!): Story!
# Remove a story
removeStory(id: String!): Story!
# Add a chapter to a story
addChapter(input: ChapterAddInput!): Chapter!
# Edit a chapter
editChapter(input: ChapterEditInput!): Chapter!
# Remove a chapter
removeChapter(id: String!): Chapter!
# Log in
@@ -16,6 +16,18 @@ type Channel {
locationName: String
}
# Filters for the channels query
input ChannelsFilter {
# Filter to either logged or unlogged channels
logged: Boolean
# Filter by event name
eventName: String
# Filter by location name
locationName: String
}
input ChannelAddInput {
# The channel's name
name: String!
+59
View File
@@ -0,0 +1,59 @@
# A Chapter is the main content body of a story.
type Chapter {
# The chapter's ID
id: String!
# The chapter's title
title: String!
# The chapter's author
author: String!
# The chapter's source
source: String!
# When the chapter was posted initialy
createdDate: String!
# The in-universe date and time of the chapter
fictionalDate: String!
# The date of edit.
editedDate: String!
}
# Input for addChapter mutation
input ChapterAddInput {
# The story to add a chapter to
storyId: String!
# The title to give the chapter. It can not be left empty
title: String!
# The author to set for the chapter. It will use the logged in user ID, and only
# users with appropriate permissions can post as another author. Even then, it's only
# for importing/moving content. This author name will not be used when logging the
# submission
author: String
# The markdown source for the content
source: String!
# Optionally, assign a fictional date for the chapter
fictionalDate: String
}
# Input for editChapter mutation
input ChapterEditInput {
# The chapter to edit
id: String!
# Change the chapter's title
title: String
# Change the source
source: String
# Set the fictional date for a chapter
fictionalDate: String
}
@@ -22,6 +22,24 @@ type Character {
description: String!
}
# Filter for characters query
input CharactersFilter {
# Filter by character IDs
ids: [String!]
# Filter by nicks
nicks: [String!]
# Filter by names
names: [String!]
# Filter by author
author: String
# Filter by text search matching against the character's description
search: String
}
# Input for adding characters
input CharacterAddInput {
# The primary IRC nick name to recognize this character by
@@ -29,6 +29,15 @@ type File {
url: String
}
# Filter for the files quiery.
input FilesFilter {
# If set, this will limit the results to either public or private files.
public: Boolean
# Limit the MIME types of the files.
mimeTypes: [String!]
}
# Input for editFile mutation
input EditFileInput {
# The file's unique ID
@@ -36,8 +36,8 @@ type Log {
posts(kinds:[String!]): [Post!]!
}
# Input for logs query
input LogQueryInput {
# Filter for logs query
input LogsFilter {
# Channels to limit results to (inclusive)
channels: [String!]
+142
View File
@@ -0,0 +1,142 @@
# 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 (RFC3339 format).
createdDate: String!
# The date the story is set in (RFC3339 format).
fictionalDate: String!
# The date of the last major update to the story (RFC3339 format). This being bumped means a new chapter has been added.
updatedDate: String!
}
# Filter for stories query.
input StoriesFilter {
# What author to query for
author: String
# What tags to query for
tags: [TagInput!]
# The earliest fictionalDate
earliestFictionalDate: String
# The latest fictionalDate
latestFictionalDate: String
# 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 (RFC3339 format).
fictionalDate: String
}
# 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 (RFC3339 format).
fictionalDate: String
}
# 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 {
# Description and content of a document or item
Document
# News stories
News
# Information about something going on in the background that may or may not inform RP
Background
# General information
Info
# A short story
Story
}
@@ -7,6 +7,15 @@ type Tag {
name: String!
}
# A Tag is a means of associating stories that have details in common with one another.
input TagInput {
# The tag's kind
kind: TagKind!
# The tag's name
name: String!
}
# Allowed values for Tag.kind
enum TagKind {
# An organization is a catch all term for in-universe corporations, teams, groups, cults, forces, etc...
+30
View File
@@ -2,6 +2,7 @@ package change
import (
"log"
"sync"
"time"
"git.aiterp.net/rpdata/api/model/counter"
@@ -30,8 +31,13 @@ var PublicModels = []string{
"Post",
}
var submitMutex sync.Mutex
// Submit submits a change to the history.
func Submit(model, op, author, objectID string, data interface{}) (Change, error) {
submitMutex.Lock()
defer submitMutex.Unlock()
index, err := counter.Next("auto_increment", "Change")
if err != nil {
return Change{}, err
@@ -47,6 +53,8 @@ func Submit(model, op, author, objectID string, data interface{}) (Change, error
Data: data,
}
Timeline.push(change)
err = collection.Insert(&change)
if err != nil {
return Change{}, err
@@ -55,6 +63,28 @@ func Submit(model, op, author, objectID string, data interface{}) (Change, error
return change, err
}
// FindID gets a change by ID
func FindID(id int) (Change, error) {
change := Change{}
err := collection.FindId(id).One(&change)
if err != nil {
return Change{}, err
}
return change, nil
}
// List gets all changes in ascending order
func List() ([]Change, error) {
changes := make([]Change, 0, 64)
err := collection.Find(nil).Sort("_id").All(&changes)
if err != nil {
return nil, err
}
return changes, nil
}
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.history")
+118
View File
@@ -0,0 +1,118 @@
package change
import (
"context"
"errors"
"sync"
"github.com/globalsign/mgo"
)
// ErrInvalidIndex is returned when a timeline subscriber tries to get a value
// with a negative id
var ErrInvalidIndex = errors.New("change.timeline: Invalid ID")
// ErrWaitRequired is returned when a timeline subscriber requests an entry that
// has not yet been pushed
var ErrWaitRequired = errors.New("change.timeline: Waiting required")
// ErrExpired is returned when a timeline subscriber requests a change that happened
// too long ago for it to be in the database.
var ErrExpired = errors.New("change.timeline: Change has expired")
type timeline struct {
mutex sync.Mutex
firstIndex int64
currentID int64
cache []Change
notifyCh chan struct{}
}
func (timeline *timeline) Get(ctx context.Context, id int64) (Change, error) {
change, err := timeline.get(id, false)
if err == ErrWaitRequired {
if err := timeline.wait(ctx); err != nil {
return Change{}, err
}
return timeline.get(id, true)
} else if err == mgo.ErrNotFound {
return Change{}, ErrExpired
}
return change, nil
}
func (timeline *timeline) push(change Change) {
timeline.mutex.Lock()
timeline.cache = append(timeline.cache, change)
if len(timeline.cache) > 128 {
copy(timeline.cache, timeline.cache[32:])
timeline.cache = timeline.cache[:len(timeline.cache)-32]
}
// Only update the currentID if this is more recent than the existing ones
if int64(change.ID) > timeline.currentID {
timeline.currentID = int64(change.ID)
}
if timeline.notifyCh != nil {
close(timeline.notifyCh)
timeline.notifyCh = nil
}
timeline.mutex.Unlock()
}
func (timeline *timeline) get(id int64, hasWaited bool) (Change, error) {
if id < 0 {
return Change{}, ErrInvalidIndex
}
timeline.mutex.Lock()
if !hasWaited && id > timeline.currentID {
timeline.mutex.Unlock()
return Change{}, ErrWaitRequired
}
for _, change := range timeline.cache {
if change.ID == int(id) {
timeline.mutex.Unlock()
return change, nil
}
}
timeline.mutex.Unlock()
return FindID(int(id))
}
func (timeline *timeline) wait(ctx context.Context) error {
timeline.mutex.Lock()
if timeline.notifyCh == nil {
timeline.notifyCh = make(chan struct{})
}
ch := timeline.notifyCh
timeline.mutex.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
case <-ch:
return nil
}
}
// Timeline has operations for subscribing to changes in real time
var Timeline = &timeline{}
// InitializeTimeline fills in the timeline with existing entries
func InitializeTimeline() {
changes, err := List()
if err != nil {
return
}
for _, change := range changes {
Timeline.push(change)
}
}
+11 -4
View File
@@ -109,13 +109,20 @@ func FindName(name string) (Channel, error) {
// List finds channels, if logged is true it will be limited to logged
// channels
func List(logged bool) ([]Channel, error) {
func List(logged *bool, eventName *string, locationName *string) ([]Channel, error) {
query := bson.M{}
if logged {
query["logged"] = true
if logged != nil {
query["logged"] = *logged
}
if eventName != nil {
query["eventName"] = *eventName
}
if locationName != nil {
query["locationName"] = *locationName
}
channels := make([]Channel, 0, 32)
channels := make([]Channel, 0, 128)
err := collection.Find(query).All(&channels)
return channels, err
+29
View File
@@ -154,6 +154,29 @@ 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) ([]Character, error) {
query := bson.M{}
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.
@@ -227,5 +250,11 @@ func init() {
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)
}
})
}
+8 -7
View File
@@ -168,21 +168,22 @@ func FindName(kind string, name string, author string) (File, error) {
// List lists files according to the standard lookup. By default it's just the author's own files,
// but if `public` is true it will alos include files made public by other authors. If `mimeTypes` contains
// any, it will limit the results to that. If `author` is empty, it will only list public files
func List(author string, public bool, mimeTypes []string) ([]File, error) {
func List(author string, public *bool, mimeTypes []string) ([]File, error) {
query := bson.M{}
if author != "" {
if public {
query["$or"] = []bson.M{
bson.M{"author": author},
bson.M{"public": true},
if public != nil {
if *public == false {
query["author"] = author
}
query["public"] = *public
} else {
query["author"] = author
}
} else {
if !public {
return nil, errors.New("No author specified, and public is unset")
if public != nil && *public == false {
return nil, errors.New("You cannot")
}
query["public"] = true
+18 -10
View File
@@ -86,16 +86,6 @@ func List(limit int) ([]Log, error) {
return listLog(bson.M{}, limit)
}
// Remove removes the log post with this ID. Both the long and short ID is accepted
func Remove(id string) error {
return logsCollection.Remove(bson.M{
"$or": []bson.M{
bson.M{"_id": id},
bson.M{"shortId": id},
},
})
}
// ListSearch lists the logs matching the parameters. Empty/zero values means the parameter is ingored when
// building the query. This is the old aitelogs2 way, but with the addition of a text search.
//
@@ -328,6 +318,24 @@ func (log *Log) UpdateCharacters() error {
return nil
}
// Remove removes the log and all associated posts from the database
func (log *Log) Remove() error {
err := logsCollection.Remove(bson.M{"_id": log.ID})
if err != nil {
return err
}
_, err = postCollection.RemoveAll(bson.M{"$or": []bson.M{
bson.M{"logId": log.ID},
bson.M{"logId": log.ShortID},
}})
if err != nil {
return err
}
return nil
}
func findLog(query interface{}) (Log, error) {
log := Log{}
err := logsCollection.Find(query).One(&log)
+20 -1
View File
@@ -41,7 +41,7 @@ func (chapter *Chapter) Edit(title, source *string, fictionalDate *time.Time) er
changes["source"] = *source
changed.Source = *source
}
if fictionalDate != nil && *fictionalDate != chapter.FictionalDate {
if fictionalDate != nil && !fictionalDate.Equal(chapter.FictionalDate) {
changes["fictionalDate"] = *fictionalDate
changed.FictionalDate = *fictionalDate
}
@@ -61,6 +61,25 @@ func (chapter *Chapter) Remove() error {
return chapterCollection.RemoveId(chapter.ID)
}
// FindChapterID finds a chapter by its own ID
func FindChapterID(id string) (Chapter, error) {
chapter := Chapter{}
err := chapterCollection.FindId(id).One(&chapter)
return chapter, err
}
// ListChapterStoryID lists all chapters for the story ID
func ListChapterStoryID(storyID string) ([]Chapter, error) {
chapters := make([]Chapter, 0, 8)
err := chapterCollection.Find(bson.M{"storyId": storyID}).Sort("createdDate").All(&chapters)
if err != nil {
return nil, err
}
return chapters, nil
}
// makeChapterID makes a random chapter ID that's 24 characters long
func makeChapterID() string {
result := "SC"
+11 -6
View File
@@ -84,23 +84,23 @@ func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDa
changes := bson.M{}
changed := *story
if name != nil && *name == story.Name {
if name != nil && *name != story.Name {
changes["name"] = *name
changed.Name = *name
}
if category != nil && *category == story.Category {
if category != nil && *category != story.Category {
changes["category"] = *category
changed.Name = *category
}
if listed != nil && *listed == story.Listed {
if listed != nil && *listed != story.Listed {
changes["listed"] = *listed
changed.Listed = *listed
}
if open != nil && *open == story.Open {
if open != nil && *open != story.Open {
changes["open"] = *open
changed.Open = *open
}
if fictionalDate != nil && *fictionalDate == story.FictionalDate {
if fictionalDate != nil && !fictionalDate.Equal(story.FictionalDate) {
changes["fictionalDate"] = *fictionalDate
changed.FictionalDate = *fictionalDate
}
@@ -124,6 +124,11 @@ func (story *Story) Remove() error {
return storyCollection.RemoveId(story.ID)
}
// Chapters calls ListChapterStoryID with the story's ID:
func (story *Story) Chapters() ([]Chapter, error) {
return ListChapterStoryID(story.ID)
}
// AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
// occurs
func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) {
@@ -215,7 +220,7 @@ func List(author string, tags []Tag, earliest, latest time.Time, limit int) ([]S
}
stories := make([]Story, 0, 128)
err := storyCollection.Find(query).Limit(limit).One(&stories)
err := storyCollection.Find(query).Limit(limit).All(&stories)
return stories, err
}
+24
View File
@@ -1,5 +1,12 @@
package story
import (
"sort"
"strings"
"github.com/globalsign/mgo/bson"
)
// A Tag associates a story with other content, like other stories, logs and more.
type Tag struct {
Kind string `bson:"kind"`
@@ -10,3 +17,20 @@ type Tag struct {
func (tag *Tag) Equal(other Tag) bool {
return tag.Kind == other.Kind && tag.Name == other.Name
}
// ListTags lists all tags
func ListTags() ([]Tag, error) {
tags := make([]Tag, 0, 64)
err := storyCollection.Find(bson.M{"listed": true, "tags": bson.M{"$ne": nil}}).Distinct("tags", &tags)
sort.Slice(tags, func(i, j int) bool {
kindCmp := strings.Compare(tags[i].Kind, tags[j].Kind)
if kindCmp != 0 {
return kindCmp < 0
}
return strings.Compare(tags[i].Name, tags[j].Name) < 0
})
return tags, err
}
-154
View File
@@ -1,154 +0,0 @@
package resolver
import (
"context"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/channel"
)
// ChannelResolver for the Channel graphql type
type ChannelResolver struct{ C channel.Channel }
// ChannelArgs is args for channel query
type ChannelArgs struct {
Name string
}
// Channel implements the channel query
func (r *QueryResolver) Channel(ctx context.Context, args *ChannelArgs) (*ChannelResolver, error) {
channel, err := channel.FindName(args.Name)
if err != nil {
return nil, err
}
return &ChannelResolver{C: channel}, nil
}
// ChannelsArgs is args for channel query
type ChannelsArgs struct {
Logged *bool
}
// Channels implements the channels query
func (r *QueryResolver) Channels(ctx context.Context, args *ChannelsArgs) ([]*ChannelResolver, error) {
channels, err := channel.List(args.Logged != nil && *args.Logged)
if err != nil {
return nil, err
}
resolvers := make([]*ChannelResolver, len(channels))
for i := range channels {
resolvers[i] = &ChannelResolver{C: channels[i]}
}
return resolvers, nil
}
// ChannelAddEditInput is input for the addChannel mutation
type ChannelAddEditInput struct {
Name string
Logged *bool
Hub *bool
EventName *string
LocationName *string
}
// AddChannel resolves the addChannel mutation
func (r *MutationResolver) AddChannel(ctx context.Context, args struct{ Input *ChannelAddEditInput }) (*ChannelResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("channel.add") {
return nil, ErrUnauthorized
}
logged := args.Input.Logged != nil && *args.Input.Logged
hub := args.Input.Hub != nil && *args.Input.Hub
eventName := ""
if args.Input.EventName != nil {
eventName = *args.Input.EventName
}
locationName := ""
if args.Input.LocationName != nil {
locationName = *args.Input.LocationName
}
channel, err := channel.New(args.Input.Name, logged, hub, eventName, locationName)
if err != nil {
return nil, err
}
return &ChannelResolver{C: channel}, nil
}
// EditChannel resolves the editChannel mutation
func (r *MutationResolver) EditChannel(ctx context.Context, args struct{ Input *ChannelAddEditInput }) (*ChannelResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("channel.edit") {
return nil, ErrUnauthorized
}
channel, err := channel.FindName(args.Input.Name)
if err != nil {
return nil, err
}
err = channel.Edit(args.Input.Logged, args.Input.Hub, args.Input.EventName, args.Input.LocationName)
if err != nil {
return nil, err
}
return &ChannelResolver{C: channel}, nil
}
// RemoveChannel resolves the editChannel mutation
func (r *MutationResolver) RemoveChannel(ctx context.Context, args ChannelArgs) (*ChannelResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("channel.remove") {
return nil, ErrUnauthorized
}
channel, err := channel.FindName(args.Name)
if err != nil {
return nil, err
}
err = channel.Remove()
if err != nil {
return nil, err
}
return &ChannelResolver{C: channel}, nil
}
// Name resolves channel.name
func (r *ChannelResolver) Name() string {
return r.C.Name
}
// Logged resolves channel.logged
func (r *ChannelResolver) Logged() bool {
return r.C.Logged
}
// Hub resolves channel.hub
func (r *ChannelResolver) Hub() bool {
return r.C.Hub
}
// EventName resolves channel.eventName
func (r *ChannelResolver) EventName() *string {
if r.C.Event == "" {
return nil
}
return &r.C.Event
}
// LocationName resolves channel.locationName
func (r *ChannelResolver) LocationName() *string {
if r.C.Location == "" {
return nil
}
return &r.C.Location
}
-330
View File
@@ -1,330 +0,0 @@
package resolver
import (
"context"
"errors"
"strings"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/loader"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/character"
"git.aiterp.net/rpdata/api/model/log"
)
// CharacterResolver for the Character graphql type
type CharacterResolver struct{ C character.Character }
// CharacterArgs is an arg
type CharacterArgs struct {
ID *string
Nick *string
}
// Character resolver
func (r *QueryResolver) Character(ctx context.Context, args *CharacterArgs) (*CharacterResolver, error) {
var char character.Character
var err error
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
switch {
case args.ID != nil:
char, err = character.FindID(*args.ID)
case args.Nick != nil:
char, err = character.FindNick(*args.Nick)
default:
err = ErrCannotResolve
}
if err != nil {
return nil, err
}
return &CharacterResolver{C: char}, nil
}
// CharactersArgs is an arg
type CharactersArgs struct {
IDs *[]string
Nicks *[]string
Author *string
}
// Characters resolves
func (r *QueryResolver) Characters(ctx context.Context, args *CharactersArgs) ([]*CharacterResolver, error) {
var chars []character.Character
var err error
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
switch {
case args.IDs != nil:
chars, err = character.ListIDs(*args.IDs...)
case args.Nicks != nil:
chars, err = character.ListNicks(*args.Nicks...)
case args.Author != nil:
chars, err = character.ListAuthor(*args.Author)
default:
chars, err = character.List()
}
if err != nil {
return nil, err
}
resolvers := make([]*CharacterResolver, 0, len(chars))
for i := range chars {
if chars[i].ID == "" {
continue
}
resolvers = append(resolvers, &CharacterResolver{C: chars[i]})
}
return resolvers, nil
}
// AddCharacterInput is args for mutation addCharacter
type AddCharacterInput struct {
Nick string
Name string
ShortName *string
Author *string
Description *string
}
// AddCharacter resolves the addCharacter mutation
func (r *MutationResolver) AddCharacter(ctx context.Context, args struct{ Input *AddCharacterInput }) (*CharacterResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member", "character.add") {
return nil, ErrUnauthorized
}
nick := args.Input.Nick
name := args.Input.Name
shortName := ""
if args.Input.ShortName != nil {
shortName = *args.Input.ShortName
} else {
shortName = strings.SplitN(args.Input.Name, " ", 2)[0]
}
author := user.ID
if args.Input.Author != nil {
author = *args.Input.Author
if author != user.ID && !user.Permitted("character.add") {
return nil, ErrPermissionDenied
}
}
description := ""
if args.Input.Description != nil {
description = *args.Input.Description
}
character, err := character.New(nick, name, shortName, author, description)
if err != nil {
return nil, err
}
go change.Submit("Character", "add", user.ID, character.ID, map[string]interface{}{
"name": character.Name,
"nick": character.Nicks[0],
"author": character.Author,
})
log.ScheduleCharacterUpdate()
return &CharacterResolver{C: character}, nil
}
// CharacterNickInput is args for mutation addCharacterNick/removeCharacterNick
type CharacterNickInput struct {
ID string
Nick string
}
// AddCharacterNick resolves the addCharacterNick mutation
func (r *MutationResolver) AddCharacterNick(ctx context.Context, args struct{ Input *CharacterNickInput }) (*CharacterResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(args.Input.ID)
if err != nil {
return nil, err
}
if character.Author != user.ID && !user.Permitted("character.edit") {
return nil, ErrPermissionDenied
}
err = character.AddNick(args.Input.Nick)
if err != nil {
return nil, err
}
go change.Submit("Character", "add.nick", user.ID, character.ID, map[string]interface{}{
"nick": args.Input.Nick,
})
log.ScheduleCharacterUpdate()
return &CharacterResolver{C: character}, nil
}
// RemoveCharacterNick resolves the removeCharacterNick mutation
func (r *MutationResolver) RemoveCharacterNick(ctx context.Context, args struct{ Input *CharacterNickInput }) (*CharacterResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(args.Input.ID)
if err != nil {
return nil, err
}
if character.Author != user.ID && !user.Permitted("character.edit") {
return nil, ErrPermissionDenied
}
err = character.RemoveNick(args.Input.Nick)
if err != nil {
return nil, err
}
go change.Submit("Character", "remove.nick", user.ID, character.ID, map[string]interface{}{
"nick": args.Input.Nick,
})
log.ScheduleCharacterUpdate()
return &CharacterResolver{C: character}, nil
}
// CharacterEditInput is args for mutation addCharacterNick/removeCharacterNick
type CharacterEditInput struct {
ID string
Name *string
ShortName *string
Description *string
}
// EditCharacter resolves the editCharacter mutation
func (r *MutationResolver) EditCharacter(ctx context.Context, args struct{ Input *CharacterEditInput }) (*CharacterResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(args.Input.ID)
if err != nil {
return nil, err
}
if character.Author != user.ID && !user.Permitted("character.edit") {
return nil, ErrPermissionDenied
}
name := ""
if args.Input.Name != nil {
name = *args.Input.Name
}
shortName := ""
if args.Input.ShortName != nil {
shortName = *args.Input.ShortName
}
description := ""
if args.Input.Description != nil {
description = *args.Input.Description
}
err = character.Edit(name, shortName, description)
if err != nil {
return nil, err
}
go change.Submit("Character", "edit", user.ID, character.ID, map[string]interface{}{
"name": character.Name,
"shortName": character.ShortName,
"description": character.Description,
})
return &CharacterResolver{C: character}, nil
}
// RemoveCharacter resolves the removeCharacter mutation
func (r *MutationResolver) RemoveCharacter(ctx context.Context, args struct{ ID string }) (*CharacterResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
character, err := character.FindID(args.ID)
if err != nil {
return nil, err
}
if character.Author != user.ID && !user.Permitted("character.remove") {
return nil, ErrPermissionDenied
}
err = character.Remove()
if err != nil {
return nil, err
}
go change.Submit("Character", "remove", user.ID, character.ID, map[string]interface{}{
"name": character.Name,
"author": character.Author,
"nicks": character.Nicks,
})
return &CharacterResolver{C: character}, nil
}
// ID is a property resolver
func (r *CharacterResolver) ID() string {
return r.C.ID
}
// Nick is a property resolver
func (r *CharacterResolver) Nick() *string {
if len(r.C.Nicks) == 0 {
return nil
}
return &r.C.Nicks[0]
}
// Nicks is a property resolver
func (r *CharacterResolver) Nicks() []string {
return r.C.Nicks
}
// Name is a property resolver
func (r *CharacterResolver) Name() string {
return r.C.Name
}
// ShortName is a property resolver
func (r *CharacterResolver) ShortName() string {
return r.C.ShortName
}
// Author is a property resolver
func (r *CharacterResolver) Author() string {
return r.C.Author
}
// Description is a property resolver
func (r *CharacterResolver) Description() string {
return r.C.Description
}
-15
View File
@@ -1,15 +0,0 @@
package resolver
import "errors"
// ErrCannotResolve is returned when a resolver constructor is at its wit's end
var ErrCannotResolve = errors.New("Cannot resolve due to invalid arguments")
// ErrNotImplemented is for TODOs
var ErrNotImplemented = errors.New("Resolver not implemented")
// ErrUnauthorized is when a guest acts like they own the place
var ErrUnauthorized = errors.New("Unauthorized")
// ErrPermissionDenied is returned when users act above their station
var ErrPermissionDenied = errors.New("Permission denied")
-163
View File
@@ -1,163 +0,0 @@
package resolver
import (
"context"
"time"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/file"
)
// FileResolver for the File graphql type
type FileResolver struct{ F file.File }
// FileArgs is an arg
type FileArgs struct {
ID string
}
// File implements the file query
func (r *QueryResolver) File(ctx context.Context, args *FileArgs) (*FileResolver, error) {
file, err := file.FindID(args.ID)
if err != nil {
return nil, err
}
return &FileResolver{F: file}, nil
}
// FilesArgs is an arg
type FilesArgs struct {
IncludePublic *bool
MimeTypes *[]string
}
// Files implements the file query
func (r *QueryResolver) Files(ctx context.Context, args *FilesArgs) ([]*FileResolver, error) {
user := session.FromContext(ctx).User()
author := ""
if user != nil {
author = user.ID
}
public := args.IncludePublic != nil && *args.IncludePublic == true
mimeTypes := []string(nil)
if args.MimeTypes != nil {
mimeTypes = *args.MimeTypes
}
files, err := file.List(author, public, mimeTypes)
if err != nil {
return nil, err
}
resolvers := make([]*FileResolver, len(files))
for i := range files {
resolvers[i] = &FileResolver{F: files[i]}
}
return resolvers, nil
}
// FileEditInput is an input for the editFile mutation
type FileEditInput struct {
ID string
Name *string
Public *bool
}
// EditFile resolves the editFile mutation
func (r *MutationResolver) EditFile(ctx context.Context, args *struct{ Input FileEditInput }) (*FileResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
file, err := file.FindID(args.Input.ID)
if err != nil {
return nil, err
}
if file.Author != user.ID && !user.Permitted("file.edit") {
return nil, ErrUnauthorized
}
err = file.Edit(args.Input.Name, args.Input.Public)
if err != nil {
return nil, err
}
return &FileResolver{F: file}, nil
}
// RemoveFile resolves the removeFIle mutation
func (r *MutationResolver) RemoveFile(ctx context.Context, args *FileArgs) (*FileResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("member") {
return nil, ErrUnauthorized
}
file, err := file.FindID(args.ID)
if err != nil {
return nil, err
}
if file.Author != user.ID && !user.Permitted("file.remove") {
return nil, ErrUnauthorized
}
err = file.Delete()
if err != nil {
return nil, err
}
return &FileResolver{F: file}, nil
}
// ID resolves File.id
func (f *FileResolver) ID() string {
return f.F.ID
}
// Author resolves File.author
func (f *FileResolver) Author() string {
return f.F.Author
}
// Kind resolves File.kind
func (f *FileResolver) Kind() string {
return f.F.Kind
}
// Time resolves File.time
func (f *FileResolver) Time() string {
return f.F.Time.Format(time.RFC3339Nano)
}
// Public resolves File.public
func (f *FileResolver) Public() bool {
return f.F.Public
}
// Name resolves File.name
func (f *FileResolver) Name() string {
return f.F.Name
}
// MimeType resolves File.mimeType
func (f *FileResolver) MimeType() string {
return f.F.MimeType
}
// Size resolves File.size
func (f *FileResolver) Size() int32 {
return int32(f.F.Size)
}
// URL resolves File.url
func (f *FileResolver) URL() *string {
if f.F.URL == "" {
return nil
}
return &f.F.URL
}
-325
View File
@@ -1,325 +0,0 @@
package resolver
import (
"context"
"errors"
"time"
"git.aiterp.net/rpdata/api/loader"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/log"
)
// LogResolver for the Log graphql type
type LogResolver struct{ L log.Log }
// LogArgs is an arg
type LogArgs struct {
ID *string
}
// LogPostArgs is an arg
type LogPostArgs struct {
Kinds *[]string
}
// Log finds log
func (r *QueryResolver) Log(ctx context.Context, args *LogArgs) (*LogResolver, error) {
var l log.Log
var err error
switch {
case args.ID != nil:
l, err = log.FindID(*args.ID)
default:
err = ErrCannotResolve
}
if err != nil {
return nil, err
}
return &LogResolver{L: l}, nil
}
// LogQueryInput is an input
type LogQueryInput struct {
Search *string
Characters *[]string
Channels *[]string
Events *[]string
Open *bool
Limit *int32
}
// Logs lists logs
func (r *QueryResolver) Logs(ctx context.Context, args *struct{ Input *LogQueryInput }) ([]*LogResolver, error) {
var logs []log.Log
var err error
input := args.Input
if input != nil {
// Parse input
limit := 100
search := ""
if input.Search != nil {
search = *input.Search
limit = 0
}
channels := []string(nil)
if input.Channels != nil {
channels = *input.Channels
limit = 0
}
characters := []string(nil)
if input.Characters != nil {
characters = *input.Characters
limit = 0
}
events := []string(nil)
if input.Events != nil {
events = *input.Events
limit = 0
}
if input.Limit != nil {
limit = int(*input.Limit)
}
open := input.Open != nil && *input.Open == true
logs, err = log.ListSearch(search, channels, characters, events, open, limit)
if err != nil {
return nil, err
}
} else {
logs, err = log.List(100)
if err != nil {
return nil, err
}
}
resolvers := make([]*LogResolver, len(logs))
for i := range logs {
resolvers[i] = &LogResolver{L: logs[i]}
}
return resolvers, nil
}
// LogAddInput is an input
type LogAddInput struct {
Date string
Channel string
Title *string
Open *bool
Event *string
Description *string
}
// AddLog resolves the addLog mutation
func (r *MutationResolver) AddLog(ctx context.Context, args *struct{ Input LogAddInput }) (*LogResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("log.add") {
return nil, ErrUnauthorized
}
date, err := time.Parse(time.RFC3339Nano, args.Input.Date)
if err != nil {
return nil, err
}
title := ""
if args.Input.Title != nil {
title = *args.Input.Title
}
event := ""
if args.Input.Event != nil {
event = *args.Input.Event
}
description := ""
if args.Input.Description != nil {
description = *args.Input.Description
}
open := args.Input.Open != nil && *args.Input.Open == true
log, err := log.New(date, args.Input.Channel, title, event, description, open)
if err != nil {
return nil, err
}
change.Submit("Log", "add", user.ID, log.ID, map[string]interface{}{
"channel": log.Channel,
"title": log.Title,
"event": log.Event,
"description": log.Description,
"open": log.Open,
})
return &LogResolver{L: log}, nil
}
// LogEditInput is an input
type LogEditInput struct {
ID string
Title *string
Event *string
Description *string
Open *bool
}
// EditLog resolves the addLog mutation
func (r *MutationResolver) EditLog(ctx context.Context, args *struct{ Input LogEditInput }) (*LogResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("log.edit") {
return nil, ErrUnauthorized
}
l, err := log.FindID(args.Input.ID)
if err != nil {
return nil, err
}
err = l.Edit(args.Input.Title, args.Input.Event, args.Input.Description, args.Input.Open)
if err != nil {
return nil, err
}
change.Submit("Log", "edit", user.ID, l.ID, map[string]interface{}{
"channel": l.Channel,
"title": args.Input.Title,
"event": args.Input.Event,
"description": args.Input.Description,
"open": args.Input.Open,
})
return &LogResolver{L: l}, nil
}
// RemoveLog resolves the removeLog mutation
func (r *MutationResolver) RemoveLog(ctx context.Context, args *struct{ ID string }) (*LogResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("log.remove") {
return nil, ErrUnauthorized
}
l, err := log.FindID(args.ID)
if err != nil {
return nil, err
}
err = log.Remove(args.ID)
if err != nil {
return nil, err
}
change.Submit("Log", "add", user.ID, l.ID, map[string]interface{}{
"channel": l.Channel,
"title": l.Title,
"event": l.Event,
"description": l.Description,
"open": l.Open,
})
return &LogResolver{L: l}, nil
}
// ID resolves Log.id
func (r *LogResolver) ID() string {
return r.L.ID
}
// ShortID resolves Log.shortId
func (r *LogResolver) ShortID() string {
return r.L.ShortID
}
// Date resolves Log.date
func (r *LogResolver) Date() string {
return r.L.Date.Format(time.RFC3339Nano)
}
// ChannelName resolves Log.channelName
func (r *LogResolver) ChannelName() string {
return r.L.Channel
}
// Channel resolves Log.channe
func (r *LogResolver) Channel(ctx context.Context) (*ChannelResolver, error) {
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
channel, err := loader.Channel("name", r.L.Channel)
if err != nil {
return nil, err
}
return &ChannelResolver{C: channel}, nil
}
// Title resolves Log.title
func (r *LogResolver) Title() string {
return r.L.Title
}
// Event resolves Log.event
func (r *LogResolver) Event() string {
return r.L.Event
}
// Description resolves Log.description
func (r *LogResolver) Description() string {
return r.L.Description
}
// Open resolves Log.open
func (r *LogResolver) Open() bool {
return r.L.Open
}
// Characters resolves Log.characters
func (r *LogResolver) Characters(ctx context.Context) ([]*CharacterResolver, error) {
loader := loader.FromContext(ctx)
if loader == nil {
return nil, errors.New("no loader")
}
chars, err := loader.Characters("id", r.L.CharacterIDs...)
if err != nil {
return nil, err
}
resolvers := make([]*CharacterResolver, 0, len(chars))
for i := range chars {
if chars[i].ID == "" {
continue
}
resolvers = append(resolvers, &CharacterResolver{C: chars[i]})
}
return resolvers, nil
}
// Posts resolves Log.posts
func (r *LogResolver) Posts(ctx context.Context, args *LogPostArgs) ([]*PostResolver, error) {
var kinds []string
if args.Kinds != nil {
kinds = *args.Kinds
}
posts, err := r.L.Posts(kinds...)
if err != nil {
return nil, err
}
resolvers := make([]*PostResolver, len(posts))
for i := range posts {
resolvers[i] = &PostResolver{posts[i]}
}
return resolvers, nil
}
-233
View File
@@ -1,233 +0,0 @@
package resolver
import (
"context"
"time"
"git.aiterp.net/rpdata/api/internal/session"
"git.aiterp.net/rpdata/api/model/change"
"git.aiterp.net/rpdata/api/model/log"
)
// PostResolver for the Post graphql type
type PostResolver struct{ P log.Post }
// PostArgs is an arg
type PostArgs struct {
ID string
}
// Post implements the post query
func (r *QueryResolver) Post(ctx context.Context, args *PostArgs) (*PostResolver, error) {
post, err := log.FindPostID(args.ID)
if err != nil {
return nil, err
}
return &PostResolver{P: post}, nil
}
// PostsArgs is an arg
type PostsArgs struct {
IDs []string
}
// Posts implements the posts query
func (r *QueryResolver) Posts(ctx context.Context, args *PostsArgs) ([]*PostResolver, error) {
posts, err := log.ListPostIDs(args.IDs...)
if err != nil {
return nil, err
}
resolvers := make([]*PostResolver, len(posts))
for i := range resolvers {
resolvers[i] = &PostResolver{P: posts[i]}
}
return resolvers, nil
}
// PostAddInput is an input
type PostAddInput struct {
LogID string
Time string
Kind string
Nick string
Text string
}
// AddPost resolves the addPost mutation
func (r *MutationResolver) AddPost(ctx context.Context, args struct{ Input *PostAddInput }) (*PostResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.add") {
return nil, ErrUnauthorized
}
postTime, err := time.Parse(time.RFC3339Nano, args.Input.Time)
if err != nil {
return nil, err
}
log, err := log.FindID(args.Input.LogID)
if err != nil {
return nil, err
}
post, err := log.NewPost(postTime, args.Input.Kind, args.Input.Nick, args.Input.Text)
if err != nil {
return nil, err
}
change.Submit("Post", "add", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
"time": post.Time,
"kind": post.Kind,
"nick": post.Nick,
"text": post.Text,
"position": post.Position,
})
go log.UpdateCharacters()
return &PostResolver{P: post}, nil
}
// PostEditInput is an input
type PostEditInput struct {
ID string
Time *string
Kind *string
Nick *string
Text *string
}
// EditPost resolves the editPost mutation
func (r *MutationResolver) EditPost(ctx context.Context, args struct{ Input *PostEditInput }) (*PostResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.edit") {
return nil, ErrUnauthorized
}
postTime := (*time.Time)(nil)
if args.Input.Time != nil {
t, err := time.Parse(time.RFC3339Nano, *args.Input.Time)
if err != nil {
return nil, err
}
postTime = &t
}
post, err := log.FindPostID(args.Input.ID)
if err != nil {
return nil, err
}
err = post.Edit(postTime, args.Input.Kind, args.Input.Nick, args.Input.Text)
if err != nil {
return nil, err
}
change.Submit("Post", "edit", user.ID, post.ID, map[string]interface{}{
"time": postTime,
"kind": args.Input.Kind,
"nick": args.Input.Nick,
"text": args.Input.Text,
})
go log.UpdateCharacters(post.LogID)
return &PostResolver{P: post}, nil
}
// PostMoveInput is an input
type PostMoveInput struct {
ID string
ToPosition int32
}
// MovePost resolves the movePost mutation
func (r *MutationResolver) MovePost(ctx context.Context, args struct{ Input *PostMoveInput }) (*PostResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.move") {
return nil, ErrUnauthorized
}
post, err := log.FindPostID(args.Input.ID)
if err != nil {
return nil, err
}
err = post.Move(int(args.Input.ToPosition))
if err != nil {
return nil, err
}
change.Submit("Post", "move", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
"targetIndex": args.Input.ToPosition,
})
return &PostResolver{P: post}, nil
}
// PostRemoveArgs is an arg
type PostRemoveArgs struct {
ID string
}
// RemovePost resolves the removePost mutation
func (r *MutationResolver) RemovePost(ctx context.Context, args PostRemoveArgs) (*PostResolver, error) {
user := session.FromContext(ctx).User()
if user == nil || !user.Permitted("post.remove") {
return nil, ErrUnauthorized
}
post, err := log.RemovePost(args.ID)
if err != nil {
return nil, err
}
change.Submit("Post", "remove", user.ID, post.ID, map[string]interface{}{
"logId": post.LogID,
})
go log.UpdateCharacters(post.LogID)
return &PostResolver{P: post}, nil
}
// ID resolves Post.id
func (r *PostResolver) ID() string {
return r.P.ID
}
// LogID resolves Post.logId
func (r *PostResolver) LogID() string {
return r.P.LogID
}
// Time resolves Post.time
func (r *PostResolver) Time() string {
return r.P.Time.Format(time.RFC3339Nano)
}
// Kind resolves Post.logId
func (r *PostResolver) Kind() string {
return r.P.Kind
}
// Nick resolves Post.nick
func (r *PostResolver) Nick() string {
return r.P.Nick
}
// Text resolves Post.text
func (r *PostResolver) Text() string {
return r.P.Text
}
// Position resolves Post.text
func (r *PostResolver) Position() int32 {
return int32(r.P.Position)
}
-13
View File
@@ -1,13 +0,0 @@
package resolver
// The RootResolver brings queries and mutations together. The rest is just for readability.
type RootResolver struct {
MutationResolver
QueryResolver
}
// The QueryResolver is the entry point for all top-level read operations.
type QueryResolver struct{}
// The MutationResolver is the entry point for all top-level mutation operations.
type MutationResolver struct{}
-51
View File
@@ -1,51 +0,0 @@
package resolver
import (
"context"
"git.aiterp.net/rpdata/api/internal/session"
)
// LoginArgs is args
type LoginArgs struct {
Username string
Password string
}
// Session resolves query.session
func (r *QueryResolver) Session(ctx context.Context) (*SessionResolver, error) {
return &SessionResolver{S: session.FromContext(ctx)}, nil
}
// Login resolves mutation.login
func (r *MutationResolver) Login(ctx context.Context, args *LoginArgs) (*SessionResolver, error) {
session := session.FromContext(ctx)
err := session.Login(args.Username, args.Password)
if err != nil {
return nil, err
}
return &SessionResolver{S: session}, nil
}
// Logout resolves mutation.logout
func (r *MutationResolver) Logout(ctx context.Context) (*SessionResolver, error) {
session := session.FromContext(ctx)
session.Logout()
return &SessionResolver{S: session}, nil
}
// SessionResolver resolves Session
type SessionResolver struct{ S *session.Session }
// User resolves Session.user
func (r *SessionResolver) User() *UserResolver {
user := r.S.User()
if user == nil {
return nil
}
return &UserResolver{U: user}
}
-1
View File
@@ -1 +0,0 @@
package resolver
+21
View File
@@ -0,0 +1,21 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+14
View File
@@ -0,0 +1,14 @@
# User Interface
## Setup
This is a UI created with `create-react-app`, so the process of getting started on a new host is navigating to the directory and running these commands.
```sh
npm install
npm start
```
## Release
This is built and packaged with the parent project.
+12036
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "rpdata-ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"ajv": "^6.5.2",
"apollo-boost": "^0.1.10",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"pluralize": "^7.0.0",
"react": "^16.4.1",
"react-apollo": "^2.1.9",
"react-dom": "^16.4.1",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"@playlyfe/gql": "^2.6.0"
},
"proxy": {
"/graphql": {
"target": "http://127.0.0.1:17000"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Aite RP</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More