Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7d5059d60 | ||
|
|
9fbdc767ad | ||
|
|
daeaaf5fa9 | ||
|
|
d3277c7476 | ||
|
|
90681c67b0 | ||
|
|
bebd99833b | ||
|
|
34f86d5037 | ||
|
|
8a85313762 | ||
|
|
017e68eae7 | ||
|
|
1955ad0517 | ||
|
|
34ba5d5d3a | ||
|
|
8c8a251080 | ||
|
|
73fe125870 | ||
|
|
04ec85aadf | ||
|
|
59233ee549 | ||
|
|
7aa0de9135 | ||
|
|
6dfbee7ff2 | ||
|
|
8a07c2ffdb | ||
|
|
0e9536b0a8 | ||
|
|
b8e5ed5813 | ||
|
|
2317f66920 | ||
|
|
fc488b938d | ||
|
|
ceb2617571 | ||
|
|
aeb5b26627 | ||
|
|
6aafc67790 | ||
|
|
c6f8fd9faa | ||
|
|
efa98b80cc | ||
|
|
707dcb43eb | ||
|
|
9bcc28a0ea | ||
|
|
2fceaa62ed | ||
|
|
86125c7ce9 | ||
|
|
bb28795cd5 | ||
|
|
815f30597e | ||
|
|
cd1f122502 | ||
|
|
0e0d802a32 | ||
|
|
3c42043ab6 | ||
|
|
8388ee2446 | ||
|
|
260c2e7f76 | ||
|
|
a4034b0faa | ||
|
|
c3a6798050 | ||
|
|
2ab933149e | ||
|
|
07b94b3cab | ||
|
|
79171bf9a2 | ||
|
|
bac57bc378 | ||
|
|
875dac9ecb | ||
|
|
0768cfd0bd | ||
|
|
2b05d90f4a | ||
|
|
0444911e73 | ||
|
|
1be4088a8f | ||
|
|
9978d7380b | ||
|
|
2e5162a29b | ||
|
|
dac0060155 | ||
|
|
9d7fd498a9 | ||
|
|
e0af2685ee | ||
|
|
3184b60310 | ||
|
|
675c09ef04 | ||
|
|
9707320960 | ||
|
|
20e53b4f5e | ||
|
|
8bb4dea358 | ||
|
|
75b2382f09 | ||
|
|
1ca25eff8e | ||
|
|
bf10ed1fe3 | ||
|
|
54136be912 | ||
|
|
228c2bfeb4 | ||
|
|
2f98be5747 | ||
|
|
2a40f97282 | ||
|
|
e5b4184106 | ||
|
|
c348a90b70 | ||
|
|
132cf28f0f | ||
|
|
d5df624188 | ||
|
|
ca98648274 | ||
|
|
4642a8abec | ||
|
|
0ec923d686 | ||
|
|
388d5378e7 | ||
|
|
67e9e8f1e6 | ||
|
|
06bc7e5a1b | ||
|
|
237190e498 | ||
|
|
b9f0fef966 |
+4
-4
@@ -6,16 +6,16 @@ pipeline:
|
||||
docker-release:
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata
|
||||
tags: latest
|
||||
auto_tag: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
when:
|
||||
event: tag
|
||||
ref: refs/tags/*
|
||||
docker-dev:
|
||||
docker-release-latest:
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata
|
||||
tags: trunk
|
||||
tags: latest
|
||||
secrets: [ docker_username, docker_password ]
|
||||
when:
|
||||
event: push
|
||||
event: tag
|
||||
ref: refs/tags/*
|
||||
+7
-1
@@ -4,4 +4,10 @@ config.json
|
||||
api
|
||||
debug
|
||||
build
|
||||
.vscode
|
||||
rpdata-graphiql
|
||||
rpdata-*
|
||||
!rpdata-*/
|
||||
.vscode
|
||||
|
||||
generated.gql
|
||||
generated.go
|
||||
+9
-22
@@ -1,41 +1,29 @@
|
||||
## 1. Server
|
||||
## 1. Build Server
|
||||
# Use Go 1.10
|
||||
FROM golang:1.10 AS build-server
|
||||
FROM golang:1.11 AS build-server
|
||||
|
||||
# Load repository into docker
|
||||
WORKDIR /go/src/git.aiterp.net/rpdata/api/
|
||||
COPY . .
|
||||
RUN rm -rf ./vendor
|
||||
|
||||
# Load build tools and dependencies
|
||||
RUN go get -u github.com/golang/dep/cmd/dep
|
||||
RUN go get -u github.com/vektah/gorunpkg
|
||||
RUN go get -u github.com/jteeuwen/go-bindata/...
|
||||
RUN dep ensure
|
||||
RUN go generate ./...
|
||||
|
||||
# Build server and tools (CGO disabled to make them 100% static)
|
||||
ENV CGO_ENABLED 0
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-graphiql ./cmd/rpdata-graphiql
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-server ./cmd/rpdata-server
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2charimport ./cmd/rpdata-lb2charimport
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2logimport ./cmd/rpdata-lb2logimport
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-wikifileimport ./cmd/rpdata-wikifileimport
|
||||
# TODO: RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-wikifileimport ./cmd/rpdata-wikifileimport
|
||||
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
|
||||
|
||||
## 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
|
||||
## 2. Distribute
|
||||
# Use alpine linux
|
||||
FROM alpine:3.7
|
||||
|
||||
@@ -44,7 +32,6 @@ RUN apk add --no-cache ca-certificates
|
||||
|
||||
# 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"]
|
||||
# The server is the main entry point
|
||||
CMD ["/usr/local/bin/rpdata-server"]
|
||||
|
||||
Generated
+83
-7
@@ -2,10 +2,32 @@
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "git.aiterp.net/aiterp/wikiauth"
|
||||
name = "github.com/99designs/gqlgen"
|
||||
packages = [
|
||||
".",
|
||||
"cmd",
|
||||
"codegen",
|
||||
"codegen/templates",
|
||||
"complexity",
|
||||
"graphql",
|
||||
"graphql/introspection",
|
||||
"handler",
|
||||
"internal/gopath"
|
||||
]
|
||||
revision = "636435b68700211441303f1a5ed92f3768ba5774"
|
||||
version = "v0.5.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/agnivade/levenshtein"
|
||||
packages = ["."]
|
||||
revision = "33860de804ddcec4fc17998745233b4ec477e7c2"
|
||||
revision = "3d21ba515fe27b856f230847e856431ae1724adc"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -37,6 +59,12 @@
|
||||
revision = "d523deb1b23d913de5bdada721a6071e71283618"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/graph-gophers/dataloader"
|
||||
packages = ["."]
|
||||
@@ -64,6 +92,15 @@
|
||||
]
|
||||
revision = "9ebf33af539ab8cb832c7107bc0a978ca8dbc0de"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/hashicorp/golang-lru"
|
||||
packages = [
|
||||
".",
|
||||
"simplelru"
|
||||
]
|
||||
revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768"
|
||||
version = "v0.5.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jmoiron/sqlx"
|
||||
@@ -103,10 +140,10 @@
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sadbox/mediawiki"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "39fea8a1336076a961a300d1d95765dcd17e8a3c"
|
||||
version = "v0.1"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
@@ -114,6 +151,26 @@
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/urfave/cli"
|
||||
packages = ["."]
|
||||
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
|
||||
version = "v1.20.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/vektah/gqlparser"
|
||||
packages = [
|
||||
".",
|
||||
"ast",
|
||||
"gqlerror",
|
||||
"lexer",
|
||||
"parser",
|
||||
"validator",
|
||||
"validator/rules"
|
||||
]
|
||||
revision = "14e83ae06ec152e6d0afb9766a00e0c0918aa8fc"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
@@ -164,15 +221,34 @@
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/tools"
|
||||
packages = [
|
||||
"go/ast/astutil",
|
||||
"go/buildutil",
|
||||
"go/internal/cgo",
|
||||
"go/loader",
|
||||
"imports",
|
||||
"internal/fastwalk"
|
||||
]
|
||||
revision = "677d2ff680c188ddb7dcd2bfa6bc7d3f2f2f75b2"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = ["cloudsql"]
|
||||
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "16fd84401bc5796ffeaa895f31d099e1a23b363b15b87386877de0829ac9575b"
|
||||
inputs-digest = "3c9430afa8b9260026925c8f496af934e2e573a2bd4587bb634fd12a20ce6c2c"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+3
-1
@@ -24,6 +24,8 @@
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
required = [ "github.com/99designs/gqlgen" ]
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/globalsign/mgo"
|
||||
@@ -38,4 +40,4 @@
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/graph-gophers/graphql-go"
|
||||
name = "github.com/graph-gophers/graphql-go"
|
||||
@@ -1,18 +1,5 @@
|
||||
# RPData API
|
||||
|
||||
Project RPData aims to centralize the AiteRP applications, and partially involve the wiki, into a common API. This will allow for easy linking and inlining between them. The primary consumers of this API will be the Logbot and the upcoming RPData website.
|
||||
Project RPData aims to centralize the AiteRP applications, and partially involve the wiki into a common API. This will allow for easy linking and inlining between them. The primary consumers of this API will be the Logbot and the upcoming RPData website.
|
||||
|
||||
## Progress
|
||||
|
||||
### Complete
|
||||
* Session
|
||||
* Character
|
||||
* Log
|
||||
* Post
|
||||
* File
|
||||
|
||||
### Remaining
|
||||
* Story
|
||||
|
||||
### Future
|
||||
* User (manual DB edits will suffice for now)
|
||||
There is some renovation going on, and the packages `model` and `graphql` are going to be superceded by `models` and `graph2`
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
"git.aiterp.net/rpdata/api/models/stories"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
@@ -71,14 +73,20 @@ func main() {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
tags := make([]story.Tag, 0, 8)
|
||||
tags := make([]models.Tag, 0, 8)
|
||||
for _, tagResult := range tagResults {
|
||||
if tagResult.PageID == result.ID {
|
||||
tags = append(tags, story.Tag{Kind: tagResult.Type, Name: tagResult.Name})
|
||||
tags = append(tags, models.Tag{Kind: models.TagKind(tagResult.Type), Name: tagResult.Name})
|
||||
}
|
||||
}
|
||||
|
||||
story, err := story.New(result.Name, result.Author[5:], result.Category, false, false, tags, publishDate, fictionalDate)
|
||||
category := models.StoryCategory(result.Category)
|
||||
if !category.IsValid() {
|
||||
log.Println(result.Name, "does not have a valid category:", result.Category)
|
||||
continue
|
||||
}
|
||||
|
||||
story, err := stories.Add(result.Name, result.Author[5:], category, true, false, tags, publishDate, fictionalDate)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
@@ -96,19 +104,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
chapter, err := story.AddChapter(title, result.Author[5:], result.Source, publishDate, fictionalDate)
|
||||
chapter, err := chapters.Add(story, 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.aiterp.net/rpdata/api/model/channel"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
"git.aiterp.net/rpdata/api/models/channels"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -17,7 +16,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
logs, err := log.List(32768)
|
||||
logs, err := logs.List(nil)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
@@ -25,18 +24,18 @@ func main() {
|
||||
|
||||
added := make(map[string]bool, 1024)
|
||||
for _, log := range logs {
|
||||
if added[log.Channel] {
|
||||
if added[log.ChannelName] {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := channel.Ensure(log.Channel, false)
|
||||
_, err := channels.Ensure(log.ChannelName, false)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, log.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
added[log.Channel] = true
|
||||
added[log.ChannelName] = true
|
||||
|
||||
fmt.Println(log.Channel, "ensured")
|
||||
fmt.Println(log.ChannelName, "ensured")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
config.json
|
||||
debug
|
||||
@@ -1,150 +0,0 @@
|
||||
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/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/file"
|
||||
logModel "git.aiterp.net/rpdata/api/model/log"
|
||||
graphql "github.com/graph-gophers/graphql-go"
|
||||
"github.com/graph-gophers/graphql-go/relay"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := store.Init()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to init store:", err)
|
||||
}
|
||||
|
||||
n, err := logModel.UpdateAllCharacters()
|
||||
if err != nil {
|
||||
log.Println("Charcter updated stopped:", err)
|
||||
}
|
||||
log.Println("Updated characters on", n, "logs")
|
||||
|
||||
schema, err := graphql.ParseSchema(schema.String(), &resolver.Resolver{}, graphql.MaxParallelism(48))
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to parse schema:", err)
|
||||
}
|
||||
|
||||
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(page)
|
||||
}))
|
||||
|
||||
relayHandler := &relay.Handler{Schema: schema}
|
||||
http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
|
||||
r = session.Load(w, r)
|
||||
|
||||
l := loader.New()
|
||||
r = r.WithContext(l.ToContext(r.Context()))
|
||||
|
||||
relayHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||
r = session.Load(w, r)
|
||||
sess := session.FromContext(r.Context())
|
||||
user := sess.User()
|
||||
if user == nil || !user.Permitted("file.upload") {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(16384); err != nil {
|
||||
http.Error(w, "Internal (1): "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
formFile, header, err := r.FormFile("file")
|
||||
if err != nil || header == nil || formFile == nil {
|
||||
http.Error(w, "No file provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := file.Upload(r.Context(), header.Filename, header.Header.Get("Content-Type"), user.ID, header.Size, formFile)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal (2): "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(file)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal (3): "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
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))
|
||||
}
|
||||
|
||||
var page = []byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.1.0/fetch.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.js"></script>
|
||||
</head>
|
||||
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
|
||||
<div id="graphiql" style="height: 100vh;">Loading...</div>
|
||||
<script>
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
return fetch("/graphql", {
|
||||
method: "post",
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: "include",
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
|
||||
document.getElementById("graphiql")
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/model/character"
|
||||
"git.aiterp.net/rpdata/api/models/characters"
|
||||
)
|
||||
|
||||
var fileName = flag.String("file", "./characters.json", "json file to load")
|
||||
@@ -31,12 +31,12 @@ func main() {
|
||||
nicksAdded, nicksFailed := 0, 0
|
||||
|
||||
for _, info := range infos {
|
||||
char, err := character.New(info.Nicks[0], info.Name, info.ShortName, info.Author, "")
|
||||
char, err := characters.Add(info.Nicks[0], info.Name, info.ShortName, info.Author, "")
|
||||
if err != nil {
|
||||
log.Println(info.Nicks[0], "failed to insert:", err)
|
||||
charsFailed++
|
||||
|
||||
char, err = character.FindNick(info.Nicks[0])
|
||||
char, err = characters.FindNick(info.Nicks[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func main() {
|
||||
}
|
||||
|
||||
for _, alt := range info.Nicks[1:] {
|
||||
err := char.AddNick(alt)
|
||||
char, err = characters.AddNick(char, alt)
|
||||
if err != nil {
|
||||
log.Println(info.Nicks[0], "failed to add nick", alt, "error:", err)
|
||||
nicksFailed++
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/posts"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
logModel "git.aiterp.net/rpdata/api/model/log"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
)
|
||||
|
||||
var prefixReplacer = strings.NewReplacer("+", "", "@", "", "!", "", "%", "")
|
||||
@@ -47,13 +49,13 @@ func main() {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
l, err := logModel.New(date, channel, title, event, "", false)
|
||||
l, err := logs.Add(date, channel, title, event, "", false)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = l.NewPost(time.Now(), "annotation.info", "rpdata-lb2logimport", "This logfile is imported from aitelogs2 and may contain errors or wrong timestamps.")
|
||||
_, err = posts.Add(l, time.Now(), "annotation.info", "rpdata-lb2logimport", "This logfile is imported from aitelogs2 and may contain errors or wrong timestamps.")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
@@ -63,7 +65,7 @@ func main() {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = l.NewPost(l.Date, "chars", prefixReplacer.Replace(line.Args[0]), line.Text)
|
||||
_, err = posts.Add(l, l.Date, "chars", prefixReplacer.Replace(line.Args[0]), line.Text)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
@@ -92,13 +94,13 @@ func main() {
|
||||
line.Verb = "SCENE"
|
||||
}
|
||||
|
||||
_, err = l.NewPost(postTime, strings.ToLower(line.Verb), prefixReplacer.Replace(line.Args[0]), line.Text)
|
||||
_, err = posts.Add(l, postTime, strings.ToLower(line.Verb), prefixReplacer.Replace(line.Args[0]), line.Text)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = l.UpdateCharacters()
|
||||
l, err = logs.UpdateCharacters(l)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/internal/loader"
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/99designs/gqlgen/handler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := store.Init()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to init store:", err)
|
||||
}
|
||||
|
||||
http.Handle("/", handler.Playground("RPData API", "/graphql"))
|
||||
http.Handle("/graphql", queryHandler())
|
||||
|
||||
go func() {
|
||||
err := logs.RunFullUpdate()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
log.Println("Characters updated")
|
||||
}()
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8081", nil))
|
||||
}
|
||||
|
||||
func queryHandler() http.HandlerFunc {
|
||||
handler := handler.GraphQL(
|
||||
graph2.New(),
|
||||
handler.RecoverFunc(func(ctx context.Context, err interface{}) error {
|
||||
// send this panic somewhere
|
||||
log.Println(err)
|
||||
log.Println(string(debug.Stack()))
|
||||
|
||||
return fmt.Errorf("shit")
|
||||
}),
|
||||
)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := loader.New()
|
||||
r = r.WithContext(l.ToContext(r.Context()))
|
||||
|
||||
// >_>
|
||||
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer of the curse") {
|
||||
w.Header().Set("X-Emerald-Herald", "Seek souls. Larger, more powerful souls.")
|
||||
w.Header().Add("X-Emerald-Herald", "Seek the king, that is the only way.")
|
||||
w.Header().Add("X-Emerald-Herald", "Lest this land swallow you whole... As it has so many others.")
|
||||
}
|
||||
|
||||
r = auth.RequestWithToken(r)
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "# Generated by stiching together the files under schema/ – DO NOT EDIT" >generated.gql
|
||||
cat schema/root.gql schema/**/*.gql >>generated.gql
|
||||
@@ -0,0 +1,57 @@
|
||||
schema: generated.gql
|
||||
|
||||
exec:
|
||||
filename: generated.go
|
||||
package: graph2
|
||||
|
||||
model:
|
||||
filename: input/generated.go
|
||||
package: input
|
||||
|
||||
models:
|
||||
Tag:
|
||||
model: git.aiterp.net/rpdata/api/models.Tag
|
||||
TagInput: #It's the same as tags
|
||||
model: git.aiterp.net/rpdata/api/models.Tag
|
||||
TagKind:
|
||||
model: git.aiterp.net/rpdata/api/models.TagKind
|
||||
Character:
|
||||
model: git.aiterp.net/rpdata/api/models.Character
|
||||
CharactersFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/characters.Filter
|
||||
Channel:
|
||||
model: git.aiterp.net/rpdata/api/models.Channel
|
||||
ChannelsFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/channels.Filter
|
||||
Post:
|
||||
model: git.aiterp.net/rpdata/api/models.Post
|
||||
PostsFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/posts.Filter
|
||||
Log:
|
||||
model: git.aiterp.net/rpdata/api/models.Log
|
||||
LogsFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/logs.Filter
|
||||
Chapter:
|
||||
model: git.aiterp.net/rpdata/api/models.Chapter
|
||||
fields:
|
||||
fictionalDate:
|
||||
resolver: true
|
||||
Story:
|
||||
model: git.aiterp.net/rpdata/api/models.Story
|
||||
fields:
|
||||
fictionalDate:
|
||||
resolver: true
|
||||
StoryCategory:
|
||||
model: git.aiterp.net/rpdata/api/models.StoryCategory
|
||||
StoriesFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/stories.Filter
|
||||
File:
|
||||
model: git.aiterp.net/rpdata/api/models.File
|
||||
FilesFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/files.Filter
|
||||
Token:
|
||||
model: git.aiterp.net/rpdata/api/models.Token
|
||||
User:
|
||||
model: git.aiterp.net/rpdata/api/models.User
|
||||
Date:
|
||||
model: git.aiterp.net/rpdata/api/models/scalars.Date
|
||||
@@ -0,0 +1,47 @@
|
||||
package graph2
|
||||
|
||||
import (
|
||||
"git.aiterp.net/rpdata/api/graph2/queries"
|
||||
"git.aiterp.net/rpdata/api/graph2/types"
|
||||
graphql "github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
//go:generate ./combine.sh
|
||||
//go:generate gorunpkg github.com/99designs/gqlgen -v
|
||||
|
||||
// New creates a new GraphQL schema.
|
||||
func New() graphql.ExecutableSchema {
|
||||
return NewExecutableSchema(Config{
|
||||
Resolvers: &rootResolver{},
|
||||
})
|
||||
}
|
||||
|
||||
type rootResolver struct{}
|
||||
|
||||
func (r *rootResolver) Query() QueryResolver {
|
||||
return &queries.Resolver
|
||||
}
|
||||
|
||||
func (r *rootResolver) Mutation() MutationResolver {
|
||||
return queries.MutationResolver
|
||||
}
|
||||
|
||||
func (r *rootResolver) Log() LogResolver {
|
||||
return &types.LogResolver
|
||||
}
|
||||
|
||||
func (r *rootResolver) Chapter() ChapterResolver {
|
||||
return &types.ChapterResolver
|
||||
}
|
||||
|
||||
func (r *rootResolver) Story() StoryResolver {
|
||||
return &types.StoryResolver
|
||||
}
|
||||
|
||||
func (r *rootResolver) File() FileResolver {
|
||||
return &types.FileResolver
|
||||
}
|
||||
|
||||
func (r *rootResolver) Token() TokenResolver {
|
||||
return &types.TokenResolver
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/channels"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Channel(ctx context.Context, name string) (models.Channel, error) {
|
||||
return channels.FindName(name)
|
||||
}
|
||||
|
||||
func (r *resolver) Channels(ctx context.Context, filter *channels.Filter) ([]models.Channel, error) {
|
||||
return channels.List(filter)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddChannel(ctx context.Context, input input.ChannelAddInput) (models.Channel, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("channel.add") {
|
||||
return models.Channel{}, errors.New("You are not permitted to add channels")
|
||||
}
|
||||
|
||||
logged := false
|
||||
if input.Logged != nil {
|
||||
logged = *input.Logged
|
||||
}
|
||||
hub := false
|
||||
if input.Hub != nil {
|
||||
hub = *input.Hub
|
||||
}
|
||||
eventName := ""
|
||||
if input.EventName != nil {
|
||||
eventName = *input.EventName
|
||||
}
|
||||
locationName := ""
|
||||
if input.LocationName != nil {
|
||||
locationName = *input.LocationName
|
||||
}
|
||||
|
||||
channel, err := channels.Add(input.Name, logged, hub, eventName, locationName)
|
||||
if err != nil {
|
||||
return models.Channel{}, errors.New("Failed to add channel: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Channel", "add", token.UserID, channel)
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditChannel(ctx context.Context, input input.ChannelEditInput) (models.Channel, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("channel.edit") {
|
||||
return models.Channel{}, errors.New("You are not permitted to edit channels")
|
||||
}
|
||||
|
||||
channel, err := channels.FindName(input.Name)
|
||||
if err != nil {
|
||||
return models.Channel{}, errors.New("Channel not found")
|
||||
}
|
||||
|
||||
channel, err = channels.Edit(channel, input.Logged, input.Hub, input.EventName, input.LocationName)
|
||||
if err != nil {
|
||||
return models.Channel{}, errors.New("Failed to edit channel: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Channel", "edit", token.UserID, channel)
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models/stories"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Chapter(ctx context.Context, id string) (models.Chapter, error) {
|
||||
return chapters.FindID(id)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddChapter(ctx context.Context, input input.ChapterAddInput) (models.Chapter, error) {
|
||||
story, err := stories.FindID(input.StoryID)
|
||||
if err != nil {
|
||||
return models.Chapter{}, errors.New("Story not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "story.add") {
|
||||
return models.Chapter{}, errors.New("Unauthorized")
|
||||
}
|
||||
|
||||
author := token.UserID
|
||||
if input.Author != nil && *input.Author != author {
|
||||
if !token.Permitted("story.add") {
|
||||
return models.Chapter{}, errors.New("False pretender")
|
||||
}
|
||||
|
||||
author = *input.Author
|
||||
}
|
||||
|
||||
if !story.Open && story.Author != author {
|
||||
return models.Chapter{}, errors.New("Story is not open")
|
||||
}
|
||||
|
||||
return chapters.Add(story, input.Title, author, input.Source, time.Now(), input.FictionalDate)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditChapter(ctx context.Context, input input.ChapterEditInput) (models.Chapter, error) {
|
||||
chapter, err := chapters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Chapter{}, errors.New("Chapter not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "chapter.edit") {
|
||||
return models.Chapter{}, errors.New("Unauthorized")
|
||||
}
|
||||
|
||||
if input.ClearFictionalDate != nil && *input.ClearFictionalDate == true {
|
||||
input.FictionalDate = &time.Time{}
|
||||
}
|
||||
|
||||
return chapters.Edit(chapter, input.Title, input.Source, input.FictionalDate)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveChapter(ctx context.Context, input input.ChapterRemoveInput) (models.Chapter, error) {
|
||||
chapter, err := chapters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Chapter{}, errors.New("Chapter not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "chapter.remove") {
|
||||
return models.Chapter{}, errors.New("Unauthorized")
|
||||
}
|
||||
|
||||
return chapters.Remove(chapter)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/characters"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Character(ctx context.Context, id *string, nick *string) (models.Character, error) {
|
||||
if id != nil {
|
||||
return characters.FindID(*id)
|
||||
} else if nick != nil {
|
||||
return characters.FindNick(*nick)
|
||||
} else {
|
||||
return models.Character{}, errors.New("You must specify either an ID or a nick")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *resolver) Characters(ctx context.Context, filter *characters.Filter) ([]models.Character, error) {
|
||||
return characters.List(filter)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddCharacter(ctx context.Context, input input.CharacterAddInput) (models.Character, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "character.add") {
|
||||
return models.Character{}, errors.New("You are not permitted to add characters")
|
||||
}
|
||||
if len(input.Name) < 2 || len(input.Nick) < 2 {
|
||||
return models.Character{}, errors.New("You need to provide a name and a nick (min length: 2)")
|
||||
}
|
||||
|
||||
shortName := ""
|
||||
if input.ShortName != nil {
|
||||
shortName = *input.ShortName
|
||||
} else {
|
||||
shortName = strings.SplitN(input.Name, " ", 2)[0]
|
||||
}
|
||||
|
||||
description := ""
|
||||
if input.Description != nil {
|
||||
description = *input.Description
|
||||
}
|
||||
|
||||
author := token.UserID
|
||||
if input.Author != nil && *input.Author != author {
|
||||
if !token.Permitted("character.add") {
|
||||
return models.Character{}, errors.New("You are only permitted to add your own characters")
|
||||
}
|
||||
|
||||
author = *input.Author
|
||||
}
|
||||
|
||||
logs.ScheduleFullUpdate()
|
||||
|
||||
character, err := characters.Add(input.Nick, input.Name, shortName, author, description)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Adding character failed: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Character", "add", token.UserID, character)
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddCharacterNick(ctx context.Context, input input.CharacterNickInput) (models.Character, error) {
|
||||
character, err := characters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Character not found")
|
||||
}
|
||||
|
||||
if len(input.Nick) < 2 {
|
||||
return models.Character{}, errors.New("You need to provide a valid nick (min length: 2)")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(character.Author, "member", "character.edit") {
|
||||
return models.Character{}, errors.New("You are not permitted to edit this character")
|
||||
}
|
||||
|
||||
logs.ScheduleFullUpdate()
|
||||
|
||||
character, err = characters.AddNick(character, input.Nick)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Failed to add nick: " + err.Error())
|
||||
}
|
||||
|
||||
go logs.ScheduleFullUpdate()
|
||||
go changes.Submit("Character", "edit", token.UserID, character)
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveCharacterNick(ctx context.Context, input input.CharacterNickInput) (models.Character, error) {
|
||||
character, err := characters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Character not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(character.Author, "member", "character.edit") {
|
||||
return models.Character{}, errors.New("You are not permitted to edit this character")
|
||||
}
|
||||
|
||||
character, err = characters.RemoveNick(character, input.Nick)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Failed to remove nick: " + err.Error())
|
||||
}
|
||||
|
||||
go logs.ScheduleFullUpdate()
|
||||
go changes.Submit("Character", "edit", token.UserID, character)
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditCharacter(ctx context.Context, input input.CharacterEditInput) (models.Character, error) {
|
||||
character, err := characters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Character not found")
|
||||
}
|
||||
|
||||
if input.Name != nil && len(*input.Name) < 2 {
|
||||
return models.Character{}, errors.New("You need to provide a valid name (min length: 2)")
|
||||
}
|
||||
if input.ShortName != nil && len(*input.ShortName) < 2 {
|
||||
return models.Character{}, errors.New("You need to provide a valid short name (min length: 2)")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(character.Author, "member", "character.edit") {
|
||||
return models.Character{}, errors.New("You are not permitted to edit this character")
|
||||
}
|
||||
|
||||
character, err = characters.Edit(character, input.Name, input.ShortName, input.Description)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Failed to edit character: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Character", "edit", token.UserID, character)
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveCharacter(ctx context.Context, input input.CharacterRemoveInput) (models.Character, error) {
|
||||
character, err := characters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Character not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(character.Author, "member", "character.remove") {
|
||||
return models.Character{}, errors.New("You are not permitted to remove this character")
|
||||
}
|
||||
|
||||
character, err = characters.Remove(character)
|
||||
if err != nil {
|
||||
return models.Character{}, errors.New("Failed to remove character: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Character", "remove", token.UserID, character)
|
||||
|
||||
return character, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/files"
|
||||
)
|
||||
|
||||
func (r *resolver) File(ctx context.Context, id string) (models.File, error) {
|
||||
return files.FindID(id)
|
||||
}
|
||||
|
||||
func (r *resolver) Files(ctx context.Context, filter *files.Filter) ([]models.File, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
if filter == nil {
|
||||
filter = &files.Filter{}
|
||||
}
|
||||
|
||||
// Only allow users to view public files that are not their own.
|
||||
if token != nil {
|
||||
if filter.Public == nil || *filter.Public == false {
|
||||
filter.Author = &token.UserID
|
||||
}
|
||||
} else {
|
||||
filter.Public = &trueValue
|
||||
}
|
||||
|
||||
return files.List(filter)
|
||||
}
|
||||
|
||||
var trueValue = true
|
||||
@@ -0,0 +1,125 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/internal/loader"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Log(ctx context.Context, id string) (models.Log, error) {
|
||||
return logs.FindID(id)
|
||||
}
|
||||
|
||||
func (r *resolver) Logs(ctx context.Context, filter *logs.Filter) ([]models.Log, error) {
|
||||
logs, err := logs.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqCtx := graphql.GetRequestContext(ctx)
|
||||
maybeCharacters := strings.Contains(reqCtx.RawQuery, "characters")
|
||||
maybeChannels := strings.Contains(reqCtx.RawQuery, "channels")
|
||||
|
||||
if len(logs) >= 100 && (maybeCharacters || maybeChannels) {
|
||||
loader := loader.FromContext(ctx)
|
||||
if loader == nil {
|
||||
return nil, errors.New("no loader")
|
||||
}
|
||||
|
||||
for _, log := range logs {
|
||||
if maybeChannels {
|
||||
loader.PrimeChannels("name", log.ChannelName)
|
||||
}
|
||||
|
||||
if maybeCharacters {
|
||||
loader.PrimeCharacters("id", log.CharacterIDs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddLog(ctx context.Context, input input.LogAddInput) (models.Log, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("log.add") {
|
||||
return models.Log{}, errors.New("You are not permitted to add logs")
|
||||
}
|
||||
|
||||
open := input.Open != nil && *input.Open == true
|
||||
title := ""
|
||||
if input.Title != nil {
|
||||
title = *input.Title
|
||||
}
|
||||
event := ""
|
||||
if input.Event != nil {
|
||||
event = *input.Event
|
||||
}
|
||||
description := ""
|
||||
if input.Description != nil {
|
||||
description = *input.Description
|
||||
}
|
||||
|
||||
log, err := logs.Add(input.Date, input.Channel, title, event, description, open)
|
||||
if !token.Authenticated() || !token.Permitted("log.add") {
|
||||
return models.Log{}, errors.New("Failed to create log: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Log", "add", token.UserID, log)
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditLog(ctx context.Context, input input.LogEditInput) (models.Log, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("log.edit") {
|
||||
return models.Log{}, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
log, err := logs.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Log{}, errors.New("Log not found")
|
||||
}
|
||||
|
||||
log, err = logs.Edit(log, input.Title, input.Event, input.Description, input.Open)
|
||||
if err != nil {
|
||||
return models.Log{}, errors.New("Failed to edit log: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Log", "edit", token.UserID, log)
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveLog(ctx context.Context, input input.LogRemoveInput) (models.Log, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("log.remove") {
|
||||
return models.Log{}, errors.New("You are not permitted to remove logs")
|
||||
}
|
||||
|
||||
log, err := logs.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Log{}, errors.New("Log not found")
|
||||
}
|
||||
|
||||
log, err = logs.Remove(log)
|
||||
if err != nil {
|
||||
return models.Log{}, errors.New("Failed to remove log: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Log", "remove", token.UserID, log)
|
||||
|
||||
return log, nil
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/posts"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Post(ctx context.Context, id string) (models.Post, error) {
|
||||
return posts.FindID(id)
|
||||
}
|
||||
|
||||
func (r *resolver) Posts(ctx context.Context, filter *posts.Filter) ([]models.Post, error) {
|
||||
// Some sanity checks to avoid querying an insame amount of posts.
|
||||
if filter == nil {
|
||||
filter = &posts.Filter{Limit: 256}
|
||||
} else {
|
||||
if (filter.Limit <= 0 || filter.Limit > 256) && filter.LogID == nil {
|
||||
return nil, errors.New("a limit of 0 (no limit) or >256 without a logId is not allowed")
|
||||
}
|
||||
|
||||
if len(filter.Kind) > 32 {
|
||||
return nil, errors.New("You cannot specify more than 32 kinds")
|
||||
}
|
||||
|
||||
if len(filter.ID) > 32 {
|
||||
return nil, errors.New("You cannot specify more than 32 IDs")
|
||||
}
|
||||
}
|
||||
|
||||
return posts.List(filter)
|
||||
}
|
||||
|
||||
// Mutation
|
||||
|
||||
func (r *mutationResolver) AddPost(ctx context.Context, input input.PostAddInput) (models.Post, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("post.add") {
|
||||
return models.Post{}, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
log, err := logs.FindID(input.LogID)
|
||||
if err != nil {
|
||||
return models.Post{}, err
|
||||
}
|
||||
|
||||
post, err := posts.Add(log, input.Time, input.Kind, input.Nick, input.Text)
|
||||
if err != nil {
|
||||
return models.Post{}, err
|
||||
}
|
||||
|
||||
go logs.UpdateCharacters(log)
|
||||
go changes.Submit("Post", "add", token.UserID, post)
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditPost(ctx context.Context, input input.PostEditInput) (models.Post, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("post.edit") {
|
||||
return models.Post{}, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
post, err := posts.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Post{}, errors.New("Post not found")
|
||||
}
|
||||
|
||||
if input.Nick != nil {
|
||||
go func() {
|
||||
log, err := logs.FindID(post.LogID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logs.UpdateCharacters(log)
|
||||
}()
|
||||
}
|
||||
|
||||
post, err = posts.Edit(post, input.Time, input.Kind, input.Nick, input.Text)
|
||||
if err != nil {
|
||||
return models.Post{}, errors.New("Adding post failed: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Post", "edit", token.UserID, post)
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovePost(ctx context.Context, input input.PostMoveInput) ([]models.Post, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("post.move") {
|
||||
return nil, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
post, err := posts.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Post not found")
|
||||
}
|
||||
|
||||
posts, err := posts.Move(post, input.ToPosition)
|
||||
if err != nil {
|
||||
return nil, errors.New("Moving posts failed: " + err.Error())
|
||||
}
|
||||
|
||||
posts2 := make([]interface{}, len(posts))
|
||||
for i := range posts {
|
||||
posts2[i] = posts[i]
|
||||
}
|
||||
go changes.Submit("Post", "move", token.UserID, posts2...)
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemovePost(ctx context.Context, input input.PostRemoveInput) (models.Post, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("post.remove") {
|
||||
return models.Post{}, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
post, err := posts.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Post{}, errors.New("Post not found (before removing, of course)")
|
||||
}
|
||||
|
||||
post, err = posts.Remove(post)
|
||||
if err != nil {
|
||||
return models.Post{}, errors.New("Could not remove post: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
log, err := logs.FindID(post.LogID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logs.UpdateCharacters(log)
|
||||
}()
|
||||
go changes.Submit("Post", "remove", token.UserID, post)
|
||||
|
||||
return post, nil
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package queries
|
||||
|
||||
type resolver struct{}
|
||||
type mutationResolver struct{}
|
||||
|
||||
// Resolver has all the queries
|
||||
var Resolver resolver
|
||||
|
||||
// MutationResolver brings the mutagens.
|
||||
var MutationResolver *mutationResolver
|
||||
@@ -0,0 +1,136 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
"git.aiterp.net/rpdata/api/models/stories"
|
||||
)
|
||||
|
||||
func (r *resolver) Story(ctx context.Context, id string) (models.Story, error) {
|
||||
return stories.FindID(id)
|
||||
}
|
||||
|
||||
func (r *resolver) Stories(ctx context.Context, filter *stories.Filter) ([]models.Story, error) {
|
||||
if filter != nil {
|
||||
if filter.Unlisted != nil && *filter.Unlisted == true {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() {
|
||||
return nil, errors.New("You are not permitted to view unlisted stories")
|
||||
}
|
||||
|
||||
if !token.Permitted("story.unlisted") {
|
||||
filter.Author = &token.UserID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stories.List(filter)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddStory(ctx context.Context, input input.StoryAddInput) (models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if token == nil || !token.Permitted("member", "story.add") {
|
||||
return models.Story{}, errors.New("Permission denied")
|
||||
}
|
||||
|
||||
author := token.UserID
|
||||
if input.Author != nil && *input.Author != author {
|
||||
if !token.Permitted("story.add") {
|
||||
return models.Story{}, errors.New("You are not permitted to add a story in another author's name")
|
||||
}
|
||||
|
||||
author = *input.Author
|
||||
}
|
||||
|
||||
fictionalDate := time.Time{}
|
||||
if input.FictionalDate != nil {
|
||||
fictionalDate = *input.FictionalDate
|
||||
}
|
||||
|
||||
listed := input.Listed != nil && *input.Listed
|
||||
open := input.Open != nil && *input.Open
|
||||
|
||||
return stories.Add(input.Name, author, input.Category, listed, open, input.Tags, time.Now(), fictionalDate)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddStoryTag(ctx context.Context, input input.StoryTagAddInput) (models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
story, err := stories.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Story{}, errors.New("Story not found")
|
||||
}
|
||||
|
||||
if !token.PermittedUser(story.Author, "member", "story.edit") {
|
||||
return models.Story{}, errors.New("You are not permitted to edit this story")
|
||||
}
|
||||
|
||||
return stories.AddTag(story, input.Tag)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveStoryTag(ctx context.Context, input input.StoryTagRemoveInput) (models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
story, err := stories.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Story{}, errors.New("Story not found")
|
||||
}
|
||||
|
||||
if !token.PermittedUser(story.Author, "member", "story.edit") {
|
||||
return models.Story{}, errors.New("You are not permitted to edit this story")
|
||||
}
|
||||
|
||||
return stories.RemoveTag(story, input.Tag)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditStory(ctx context.Context, input input.StoryEditInput) (models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
story, err := stories.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Story{}, errors.New("Story not found")
|
||||
}
|
||||
|
||||
if !token.PermittedUser(story.Author, "member", "story.edit") {
|
||||
return models.Story{}, errors.New("You are not permitted to remove this story")
|
||||
}
|
||||
|
||||
if input.ClearFictionalDate != nil && *input.ClearFictionalDate {
|
||||
input.FictionalDate = &time.Time{}
|
||||
}
|
||||
|
||||
return stories.Edit(story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveStory(ctx context.Context, input input.StoryRemoveInput) (models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
story, err := stories.FindID(input.ID)
|
||||
if err != nil {
|
||||
return models.Story{}, errors.New("Story not found")
|
||||
}
|
||||
|
||||
if !token.PermittedUser(story.Author, "member", "story.remove") {
|
||||
return models.Story{}, errors.New("You are not permitted to remove this story")
|
||||
}
|
||||
|
||||
story, err = stories.Remove(story)
|
||||
if err != nil {
|
||||
return models.Story{}, err
|
||||
}
|
||||
|
||||
err = chapters.RemoveStory(story)
|
||||
if err != nil {
|
||||
return models.Story{}, errors.New("Failed to remove chapters, but story is removed: " + err.Error())
|
||||
}
|
||||
|
||||
return story, nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/tags"
|
||||
)
|
||||
|
||||
func (r *resolver) Tags(ctx context.Context) ([]models.Tag, error) {
|
||||
return tags.List()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
func (r *resolver) Token(ctx context.Context) (models.Token, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() {
|
||||
return models.Token{}, errors.New("No (valid) token")
|
||||
}
|
||||
|
||||
return *token, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
# Find character by either an ID or a nick.
|
||||
character(id: String, nick: String): Character!
|
||||
|
||||
# Find characters
|
||||
characters(filter: CharactersFilter): [Character!]!
|
||||
|
||||
|
||||
# Find channel by name
|
||||
channel(name: String!): Channel!
|
||||
|
||||
# Find channels
|
||||
channels(filter: ChannelsFilter): [Channel!]!
|
||||
|
||||
|
||||
# Find post by ID.
|
||||
post(id: String!): Post!
|
||||
|
||||
# Find posts
|
||||
posts(filter: PostsFilter): [Post!]!
|
||||
|
||||
|
||||
# Find log by ID
|
||||
log(id: String!): Log!
|
||||
|
||||
# Find logs
|
||||
logs(filter: LogsFilter): [Log!]!
|
||||
|
||||
|
||||
# Find story chapter by ID
|
||||
chapter(id: String!): Chapter!
|
||||
|
||||
|
||||
# Find all distinct tags used in stories
|
||||
tags: [Tag!]!
|
||||
|
||||
|
||||
# Find story by ID
|
||||
story(id: String!): Story!
|
||||
|
||||
# Find stories
|
||||
stories(filter: StoriesFilter): [Story!]!
|
||||
|
||||
|
||||
# Find file by ID
|
||||
file(id: String!): File!
|
||||
|
||||
# Find files
|
||||
files(filter: FilesFilter): [File!]!
|
||||
|
||||
|
||||
# Get information about the token, useful for debugging.
|
||||
token: Token!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# 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(input: StoryRemoveInput!): Story!
|
||||
|
||||
|
||||
# Add a chapter to a story
|
||||
addChapter(input: ChapterAddInput!): Chapter!
|
||||
|
||||
# Edit a chapter
|
||||
editChapter(input: ChapterEditInput!): Chapter!
|
||||
|
||||
# Remove a chapter
|
||||
removeChapter(input: ChapterRemoveInput!): Chapter!
|
||||
|
||||
|
||||
# Add a new log
|
||||
addLog(input: LogAddInput!): Log!
|
||||
|
||||
# Edit a log
|
||||
editLog(input: LogEditInput!): Log!
|
||||
|
||||
# Remove a log
|
||||
removeLog(input: LogRemoveInput!): Log!
|
||||
|
||||
|
||||
# Add a post
|
||||
addPost(input: PostAddInput!): Post!
|
||||
|
||||
# Edit a post
|
||||
editPost(input: PostEditInput!): Post!
|
||||
|
||||
# Move a post. All affected posts will be returned.
|
||||
movePost(input: PostMoveInput!): [Post!]!
|
||||
|
||||
# Remove a post
|
||||
removePost(input: PostRemoveInput!): Post!
|
||||
|
||||
|
||||
# Add a new character
|
||||
addCharacter(input: CharacterAddInput!): Character!
|
||||
|
||||
# Add nick to character
|
||||
addCharacterNick(input: CharacterNickInput!): Character!
|
||||
|
||||
# Remove nick from character
|
||||
removeCharacterNick(input: CharacterNickInput!): Character!
|
||||
|
||||
# Edit character
|
||||
editCharacter(input: CharacterEditInput!): Character!
|
||||
|
||||
# Remove a character
|
||||
removeCharacter(input: CharacterRemoveInput!): Character!
|
||||
|
||||
|
||||
# Add a channel
|
||||
addChannel(input: ChannelAddInput!): Channel!
|
||||
|
||||
# Edit a channel
|
||||
editChannel(input: ChannelEditInput!): Channel!
|
||||
}
|
||||
|
||||
# A Date represents a RFC3339 encoded date with up to millisecond precision.
|
||||
scalar Date
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Information about an IRC channel
|
||||
type Channel {
|
||||
# The channel's name
|
||||
name: String!
|
||||
|
||||
# Whether the channel should be logged
|
||||
logged: Boolean!
|
||||
|
||||
# Whether the channel is a hub channel
|
||||
hub: Boolean!
|
||||
|
||||
# The event name, or `null` if none is specified
|
||||
eventName: String
|
||||
|
||||
# The location name, or `null` if none is specified
|
||||
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!
|
||||
|
||||
# Whether the channel should be logged
|
||||
logged: Boolean
|
||||
|
||||
# Whether the channel is a hub channel
|
||||
hub: Boolean
|
||||
|
||||
# The event name, or `null` if none is specified
|
||||
eventName: String
|
||||
|
||||
# The location name, or `null` if none is specified
|
||||
locationName: String
|
||||
}
|
||||
|
||||
input ChannelEditInput {
|
||||
# The channel's name
|
||||
name: String!
|
||||
|
||||
# Whether the channel should be logged
|
||||
logged: Boolean
|
||||
|
||||
# Whether the channel is a hub channel
|
||||
hub: Boolean
|
||||
|
||||
# The event name, or `null` if none is specified
|
||||
eventName: String
|
||||
|
||||
# The location name, or `null` if none is specified
|
||||
locationName: String
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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: Date!
|
||||
|
||||
# The in-universe date and time of the chapter
|
||||
fictionalDate: Date
|
||||
|
||||
# The date of edit.
|
||||
editedDate: Date!
|
||||
}
|
||||
|
||||
# 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: Date
|
||||
}
|
||||
|
||||
# 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: Date
|
||||
|
||||
# Remove the fictional date for a chapter
|
||||
clearFictionalDate: Boolean
|
||||
}
|
||||
|
||||
# Input for removeChapter mutation
|
||||
input ChapterRemoveInput {
|
||||
# The chapter to remove
|
||||
id: String!
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# A Character represents an RP character
|
||||
type Character {
|
||||
# A unique identifier for the character
|
||||
id: String!
|
||||
|
||||
# The primary IRC nick belonging to the character
|
||||
nick: String
|
||||
|
||||
# All IRC nicks associated with this character
|
||||
nicks: [String!]!
|
||||
|
||||
# The character's author
|
||||
author: String!
|
||||
|
||||
# The character's name
|
||||
name: String!
|
||||
|
||||
# The name to display when space is scarce, usually the first/given name
|
||||
shortName: String!
|
||||
|
||||
# A short description of the 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
|
||||
nick: String!
|
||||
|
||||
# The character's name
|
||||
name: String!
|
||||
|
||||
# Optioanl shortened name. By default, it uses the first token in the name
|
||||
shortName: String
|
||||
|
||||
# Description for a character.
|
||||
description: String
|
||||
|
||||
# Optioanlly, specify another author. This needs special permissions if it's not
|
||||
# your own username
|
||||
author: String
|
||||
}
|
||||
|
||||
# Input for addNick and removeNick mutation
|
||||
input CharacterNickInput {
|
||||
# The ID of the character
|
||||
id: String!
|
||||
|
||||
# The nick to add or remove
|
||||
nick: String!
|
||||
}
|
||||
|
||||
# Inpuit for editCharacter mutation
|
||||
input CharacterEditInput {
|
||||
# The id for the character to edit
|
||||
id: String!
|
||||
|
||||
# The full name of the character -- not the salarian full name!
|
||||
name: String
|
||||
|
||||
# The character's short name that is used in compact lists
|
||||
shortName: String
|
||||
|
||||
# A short description for the character
|
||||
description: String
|
||||
}
|
||||
|
||||
# Input for removeCharacter mutation
|
||||
input CharacterRemoveInput {
|
||||
# The id for the character to edit
|
||||
id: String!
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# A File contains information about a file and where to download it.
|
||||
type File {
|
||||
# The file's unique ID
|
||||
id: String!
|
||||
|
||||
# The kind of file. Most will be "upload", but some that are ported from the wiki will have other values for this.
|
||||
kind: String!
|
||||
|
||||
# The time of uploading
|
||||
time: Date!
|
||||
|
||||
# Whether the file is publicly listable. Someone with knowledge of the ID
|
||||
# will still be able to view it, however.
|
||||
public: Boolean!
|
||||
|
||||
# The file's name
|
||||
name: String!
|
||||
|
||||
# The MIME type of the file
|
||||
mimeType: String!
|
||||
|
||||
# The file's size in bytes
|
||||
size: Int!
|
||||
|
||||
# The uploader
|
||||
author: String!
|
||||
|
||||
# The URL where the file is hosted
|
||||
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.
|
||||
mimeType: [String!]
|
||||
}
|
||||
|
||||
# Input for editFile mutation
|
||||
input EditFileInput {
|
||||
# The file's unique ID
|
||||
id: String!
|
||||
|
||||
# Whether the file is publicly listable. Someone with knowledge of the ID
|
||||
# will still be able to view it, however.
|
||||
public: Boolean
|
||||
|
||||
# The file's name
|
||||
name: String
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
# A Log is the "file" of an RP session
|
||||
type Log {
|
||||
# A unique identifier for the log.
|
||||
id: String!
|
||||
|
||||
# A secondary unique identifier for the log. This is a lot shorter and more suitable for storage when
|
||||
# the link doesn't need to be as expressive.
|
||||
shortId: String!
|
||||
|
||||
# The date for a log.
|
||||
date: Date!
|
||||
|
||||
# The channel of a log.
|
||||
channelName: String!
|
||||
|
||||
# Information about the channel this log is in.
|
||||
channel: Channel!
|
||||
|
||||
# The session's title.
|
||||
title: String!
|
||||
|
||||
# The log's event, which is the same as the tags in the previous logbot site.
|
||||
# Empty string means that it's no event.
|
||||
eventName: String!
|
||||
|
||||
# The description of a session, which is empty if unset.
|
||||
description: String!
|
||||
|
||||
# Whether the log session is open.
|
||||
open: Boolean!
|
||||
|
||||
# The characters involved in the log file.
|
||||
characters: [Character!]!
|
||||
|
||||
# The posts of the logfile, which can be filtered by kinds.
|
||||
posts(kinds:[String!]): [Post!]!
|
||||
}
|
||||
|
||||
# Filter for logs query
|
||||
input LogsFilter {
|
||||
# Channels to limit results to (inclusive)
|
||||
channels: [String!]
|
||||
|
||||
# Events to limit results to (inclusive)
|
||||
events: [String!]
|
||||
|
||||
# Characters to limit results to (exclusive)
|
||||
characters: [String!]
|
||||
|
||||
# Search post content
|
||||
search: String
|
||||
|
||||
# Limit by whether it's open or not
|
||||
open: Boolean
|
||||
|
||||
# Limit the amount of results you get back
|
||||
limit: Int
|
||||
}
|
||||
|
||||
# Input for addLog mutation
|
||||
input LogAddInput {
|
||||
# The date of the log.
|
||||
date: Date!
|
||||
|
||||
# The channel where the log is set
|
||||
channel: String!
|
||||
|
||||
# Optional: The log title
|
||||
title: String
|
||||
|
||||
# Optional: Whether the log is open
|
||||
open: Boolean
|
||||
|
||||
# Optional: The log event name
|
||||
event: String
|
||||
|
||||
# Optional: A short description of the log
|
||||
description: String
|
||||
}
|
||||
|
||||
# Input for addLog mutation
|
||||
input LogEditInput {
|
||||
# The id of the log
|
||||
id: String!
|
||||
|
||||
# The log title
|
||||
title: String
|
||||
|
||||
# The log event name
|
||||
event: String
|
||||
|
||||
# A short description of the log
|
||||
description: String
|
||||
|
||||
# Open/close the log
|
||||
open: Boolean
|
||||
}
|
||||
|
||||
# Input for removeLog mutation
|
||||
input LogRemoveInput {
|
||||
# The id of the log to remove
|
||||
id: String!
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# A Post is a part of a log
|
||||
type Post {
|
||||
# The post's ID
|
||||
id: String!
|
||||
|
||||
# The post's Log ID. This is the closest thing to a link back since this API graph doesn't have any cycles.
|
||||
logId: String!
|
||||
|
||||
# The date and time of posting
|
||||
time: Date!
|
||||
|
||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||
kind: String!
|
||||
|
||||
# The character nick
|
||||
nick: String!
|
||||
|
||||
# The post's text, which purpose depends on the kind
|
||||
text: String!
|
||||
|
||||
# The post's position, used for reordering
|
||||
position: Int!
|
||||
}
|
||||
|
||||
# Input for the addPost mutation
|
||||
input PostAddInput {
|
||||
# The log's ID that this post should be a part of
|
||||
logId: String!
|
||||
|
||||
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
||||
time: Date!
|
||||
|
||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||
kind: String!
|
||||
|
||||
# The character nick, or command invoker for non-RP stuff
|
||||
nick: String!
|
||||
|
||||
# The post's text, which purpose depends on the kind
|
||||
text: String!
|
||||
}
|
||||
|
||||
# Input for the editPost mutation
|
||||
input PostEditInput {
|
||||
# The Post ID
|
||||
id: String!
|
||||
|
||||
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
||||
time: Date
|
||||
|
||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||
kind: String
|
||||
|
||||
# The character nick, or command invoker for non-RP stuff
|
||||
nick: String
|
||||
|
||||
# The post's text, which purpose depends on the kind
|
||||
text: String
|
||||
}
|
||||
|
||||
# Input for the movePost mutation
|
||||
input PostMoveInput {
|
||||
# The Post ID
|
||||
id: String!
|
||||
|
||||
# Target index
|
||||
toPosition: Int!
|
||||
}
|
||||
|
||||
# Input for the removePost mutation
|
||||
input PostRemoveInput {
|
||||
# The Post ID
|
||||
id: String!
|
||||
}
|
||||
|
||||
# Filter for posts query
|
||||
input PostsFilter {
|
||||
id: [String!]
|
||||
kind: [String!]
|
||||
logId: String
|
||||
limit: Int
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
# A Story is a piece of content that, unlike wiki articles, are not something that should be ordered by time instead of title. It can
|
||||
# contain multiple chapters by one or more autohrs
|
||||
type Story {
|
||||
# The story's ID
|
||||
id: String!
|
||||
|
||||
# The story's name, which will show up in the lists.
|
||||
name: String!
|
||||
|
||||
# The story's author
|
||||
author: String!
|
||||
|
||||
# Whether other users may add/edit chapters to the story.
|
||||
open: Boolean!
|
||||
|
||||
# Whether users without a direct link can find this story.
|
||||
listed: Boolean!
|
||||
|
||||
# The category of the story
|
||||
category: StoryCategory!
|
||||
|
||||
# The tags for this tory
|
||||
tags: [Tag!]!
|
||||
|
||||
# The chapters of this story
|
||||
chapters: [Chapter!]!
|
||||
|
||||
# The date the story was created.
|
||||
createdDate: Date!
|
||||
|
||||
# The date the story is set in.
|
||||
fictionalDate: Date
|
||||
|
||||
# The date of the last major update to the story. This being bumped means a new chapter has been added.
|
||||
updatedDate: Date!
|
||||
}
|
||||
|
||||
# Filter for stories query.
|
||||
input StoriesFilter {
|
||||
# What author to query for
|
||||
author: String
|
||||
|
||||
# What category to query for
|
||||
category: StoryCategory
|
||||
|
||||
# What tags to query for
|
||||
tags: [TagInput!]
|
||||
|
||||
# If true, it will only show unlisted page you have access to
|
||||
unlisted: Boolean
|
||||
|
||||
# Whether the page is open for additions by other users
|
||||
open: Boolean
|
||||
|
||||
# The earliest fictionalDate
|
||||
earliestFictionalDate: Date
|
||||
|
||||
# The latest fictionalDate
|
||||
latestFictionalDate: Date
|
||||
|
||||
# The max amount of stories to get (default: 30)
|
||||
limit: Int
|
||||
}
|
||||
|
||||
# Input for the addStory mutation
|
||||
input StoryAddInput {
|
||||
# Set the name of the story, which will show up as the title, and as a suggestion for
|
||||
# the first chapter being added.
|
||||
name: String!
|
||||
|
||||
# Set the category for the new story.
|
||||
category: StoryCategory!
|
||||
|
||||
# Add the story under another name. This requires the story.add permission, and should not be
|
||||
# abused. The action will be logged with the actual creator's user ID.
|
||||
author: String
|
||||
|
||||
# Allow other users to post chapters to the story.
|
||||
open: Boolean
|
||||
|
||||
# Allow other users to see this page in any lists or search results.
|
||||
listed: Boolean
|
||||
|
||||
# Set which tags the story should have.
|
||||
tags: [TagInput!]
|
||||
|
||||
# Set the fictional date of the story.
|
||||
fictionalDate: Date
|
||||
}
|
||||
|
||||
# Input for the editStory mutation
|
||||
input StoryEditInput {
|
||||
# What story to edit
|
||||
id: String!
|
||||
|
||||
# Set the name of the story, which will show up as the title, and as a suggestion for
|
||||
# the first chapter being added.
|
||||
name: String
|
||||
|
||||
# Set the category for the new story.
|
||||
category: StoryCategory
|
||||
|
||||
# Add the story under another name. This requires the story.add permission, and should not be
|
||||
# abused. The action will be logged with the actual creator's user ID.
|
||||
author: String
|
||||
|
||||
# Change whether to allow others to add chapters
|
||||
open: Boolean
|
||||
|
||||
# Change whether to show this story in the list and search results
|
||||
listed: Boolean
|
||||
|
||||
# Set the fictional date of the story.
|
||||
fictionalDate: Date
|
||||
|
||||
# Clear the fictional date of the story.
|
||||
clearFictionalDate: Boolean
|
||||
}
|
||||
|
||||
# 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!
|
||||
}
|
||||
|
||||
# Input for the removeStory mutation
|
||||
input StoryRemoveInput {
|
||||
# What story to remove.
|
||||
id: String!
|
||||
}
|
||||
|
||||
# Possible values for Story.category
|
||||
enum StoryCategory {
|
||||
# General information
|
||||
Info
|
||||
|
||||
# News stories
|
||||
News
|
||||
|
||||
# Description and content of a document or item
|
||||
Document
|
||||
|
||||
# Information about something going on in the background that may or may not inform RP
|
||||
Background
|
||||
|
||||
# A short story
|
||||
Story
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# A Tag is a means of associating stories that have details in common with one another.
|
||||
type Tag {
|
||||
# The tag's kind
|
||||
kind: TagKind!
|
||||
|
||||
# The tag's name
|
||||
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...
|
||||
Organization
|
||||
|
||||
# A character tag should have the exact full name of the character.
|
||||
Character
|
||||
|
||||
# A location is anything from a planet to an establishment. This may overlap with an organization, and if so, both
|
||||
# kinds of tags should be used.
|
||||
Location
|
||||
|
||||
# An event is a plot or a part of a plot.
|
||||
Event
|
||||
|
||||
# None of the above, but it does still tie multiple stories together. The new story/chapter format may obsolete this tag kind.
|
||||
Series
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Token represents the data parsed and validated from the bearer token.
|
||||
type Token {
|
||||
# The user represented in the token.
|
||||
user: User!
|
||||
|
||||
# The permissions for the token.
|
||||
permissions: [String!]!
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# A user represents a member on the RPData platform.
|
||||
type User {
|
||||
# The user's unique ID / username
|
||||
id: String!
|
||||
|
||||
# What the user is permitted to do.
|
||||
permissions: [String!]!
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
type chapterResolver struct{}
|
||||
|
||||
func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Chapter) (*time.Time, error) {
|
||||
if chapter.FictionalDate.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &chapter.FictionalDate, nil
|
||||
}
|
||||
|
||||
// ChapterResolver is a resolver
|
||||
var ChapterResolver chapterResolver
|
||||
@@ -0,0 +1,16 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
type fileResolver struct{}
|
||||
|
||||
func (r *fileResolver) Size(ctx context.Context, file *models.File) (int, error) {
|
||||
return int(file.Size), nil
|
||||
}
|
||||
|
||||
// FileResolver is a resolver
|
||||
var FileResolver fileResolver
|
||||
@@ -0,0 +1,41 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/loader"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/posts"
|
||||
)
|
||||
|
||||
type logResolver struct{}
|
||||
|
||||
func (r *logResolver) Channel(ctx context.Context, log *models.Log) (models.Channel, error) {
|
||||
loader := loader.FromContext(ctx)
|
||||
if loader == nil {
|
||||
return models.Channel{}, errors.New("no loader")
|
||||
}
|
||||
|
||||
return loader.Channel("name", log.ChannelName)
|
||||
}
|
||||
|
||||
func (r *logResolver) Characters(ctx context.Context, log *models.Log) ([]models.Character, error) {
|
||||
loader := loader.FromContext(ctx)
|
||||
if loader == nil {
|
||||
return nil, errors.New("no loader")
|
||||
}
|
||||
|
||||
return loader.Characters("id", log.CharacterIDs...)
|
||||
}
|
||||
|
||||
func (r *logResolver) Posts(ctx context.Context, log *models.Log, kinds []string) ([]models.Post, error) {
|
||||
return posts.List(&posts.Filter{
|
||||
LogID: &log.ShortID,
|
||||
Kind: kinds,
|
||||
Limit: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// LogResolver is a resolver
|
||||
var LogResolver logResolver
|
||||
@@ -0,0 +1,27 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
type storyResolver struct{}
|
||||
|
||||
func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) {
|
||||
if story.FictionalDate.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &story.FictionalDate, nil
|
||||
}
|
||||
|
||||
func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]models.Chapter, error) {
|
||||
return chapters.ListStoryID(story.ID)
|
||||
}
|
||||
|
||||
// StoryResolver is a resolver
|
||||
var StoryResolver storyResolver
|
||||
@@ -0,0 +1,17 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/users"
|
||||
)
|
||||
|
||||
type tokenResolver struct{}
|
||||
|
||||
func (r *tokenResolver) User(ctx context.Context, token *models.Token) (models.User, error) {
|
||||
return users.Find(token.UserID)
|
||||
}
|
||||
|
||||
// TokenResolver is a resolver
|
||||
var TokenResolver tokenResolver
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/channel"
|
||||
)
|
||||
@@ -24,8 +24,8 @@ type ChannelAddArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("channel.add") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (r *MutationResolver) AddChannel(ctx context.Context, args *ChannelAddArgs)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Channel", "add", user.ID, channel.Name, map[string]interface{}{
|
||||
go change.Submit("Channel", "add", token.UserID, channel.Name, map[string]interface{}{
|
||||
"logged": channel.Logged,
|
||||
"hub": channel.Hub,
|
||||
"location": input.LocationName,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
@@ -25,8 +25,8 @@ type AddChapterArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "chapter.add") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ func (r *MutationResolver) AddChapter(ctx context.Context, args *AddChapterArgs)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
author := user.ID
|
||||
author := token.UserID
|
||||
if input.Author != nil {
|
||||
author = *input.Author
|
||||
|
||||
if user.ID != author && !user.Permitted("chapter.add") {
|
||||
if token.UserID != author && !token.Permitted("chapter.add") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (r *MutationResolver) AddChapter(ctx context.Context, args *AddChapterArgs)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Chapter", "add", user.ID, chapter.ID, map[string]interface{}{
|
||||
go change.Submit("Chapter", "add", token.UserID, chapter.ID, map[string]interface{}{
|
||||
"title": input.Title,
|
||||
"author": author,
|
||||
"fictionalDate": fictionalDate,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/character"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
@@ -23,8 +23,8 @@ type AddCharacterInput struct {
|
||||
// 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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "character.add") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ func (r *MutationResolver) AddCharacter(ctx context.Context, args struct{ Input
|
||||
shortName = strings.SplitN(input.Name, " ", 2)[0]
|
||||
}
|
||||
|
||||
author := user.ID
|
||||
author := token.UserID
|
||||
if input.Author != nil {
|
||||
author = *input.Author
|
||||
|
||||
if author != user.ID && !user.Permitted("character.add") {
|
||||
if author != token.UserID && !token.Permitted("character.add") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (r *MutationResolver) AddCharacter(ctx context.Context, args struct{ Input
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Character", "add", user.ID, character.ID, map[string]interface{}{
|
||||
go change.Submit("Character", "add", token.UserID, character.ID, map[string]interface{}{
|
||||
"name": character.Name,
|
||||
"nick": character.Nicks[0],
|
||||
"author": character.Author,
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/character"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
@@ -20,8 +20,8 @@ type AddCharacterNickInput struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func (r *MutationResolver) AddCharacterNick(ctx context.Context, args struct{ In
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if character.Author != user.ID && !user.Permitted("character.edit") {
|
||||
if character.Author != token.UserID && !token.Permitted("character.edit") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func (r *MutationResolver) AddCharacterNick(ctx context.Context, args struct{ In
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Character", "add.nick", user.ID, character.ID, map[string]interface{}{
|
||||
go change.Submit("Character", "add.nick", token.UserID, character.ID, map[string]interface{}{
|
||||
"nick": input.Nick,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
)
|
||||
@@ -26,8 +26,8 @@ type LogAddArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("log.add") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (r *MutationResolver) AddLog(ctx context.Context, args *LogAddArgs) (*types
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Log", "add", user.ID, log.ID, map[string]interface{}{
|
||||
go change.Submit("Log", "add", token.UserID, log.ID, map[string]interface{}{
|
||||
"channel": log.Channel,
|
||||
"title": log.Title,
|
||||
"event": log.Event,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
)
|
||||
@@ -25,8 +25,8 @@ type PostAddArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("post.add") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (r *MutationResolver) AddPost(ctx context.Context, args *PostAddArgs) (*typ
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Post", "add", user.ID, post.ID, map[string]interface{}{
|
||||
go change.Submit("Post", "add", token.UserID, post.ID, map[string]interface{}{
|
||||
"logId": post.LogID,
|
||||
"time": post.Time,
|
||||
"kind": post.Kind,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
@@ -30,16 +30,16 @@ type StoryAddArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "story.add") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
author := user.ID
|
||||
author := token.UserID
|
||||
if input.Author != nil {
|
||||
author = *input.Author
|
||||
|
||||
if user.ID != author && !user.Permitted("story.add") {
|
||||
if token.UserID != author && !token.Permitted("story.add") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (r *MutationResolver) AddStory(ctx context.Context, args *StoryAddArgs) (*t
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Story", "add", user.ID, story.ID, map[string]interface{}{
|
||||
go change.Submit("Story", "add", token.UserID, story.ID, map[string]interface{}{
|
||||
"name": input.Name,
|
||||
"category": input.Category,
|
||||
"author": input.Author,
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
@@ -25,8 +25,8 @@ func (r *MutationResolver) AddStoryTag(ctx context.Context, args *StoryTagAddArg
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "story.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func (r *MutationResolver) AddStoryTag(ctx context.Context, args *StoryTagAddArg
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if story.Author != user.ID && !user.Permitted("story.edit") {
|
||||
if story.Author != token.UserID && !token.Permitted("story.edit") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (r *MutationResolver) AddStoryTag(ctx context.Context, args *StoryTagAddArg
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Story", "add.tag", user.ID, story.ID, map[string]interface{}{
|
||||
go change.Submit("Story", "add.tag", token.UserID, story.ID, map[string]interface{}{
|
||||
"kind": tag.Kind,
|
||||
"name": tag.Name,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/channel"
|
||||
)
|
||||
@@ -24,8 +24,8 @@ type ChannelEditArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("channel.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (r *MutationResolver) EditChannel(ctx context.Context, args *ChannelEditArg
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Channel", "edit", user.ID, channel.Name, map[string]interface{}{
|
||||
go change.Submit("Channel", "edit", token.UserID, channel.Name, map[string]interface{}{
|
||||
"logged": input.Logged,
|
||||
"hub": input.Hub,
|
||||
"location": input.LocationName,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
@@ -24,8 +24,8 @@ type EditChapterArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "chapter.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (r *MutationResolver) EditChapter(ctx context.Context, args *EditChapterArg
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if chapter.Author != user.ID && !user.Permitted("chapter.edit") {
|
||||
if chapter.Author != token.UserID && !token.Permitted("chapter.edit") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (r *MutationResolver) EditChapter(ctx context.Context, args *EditChapterArg
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Chapter", "edit", user.ID, chapter.ID, map[string]interface{}{
|
||||
go change.Submit("Chapter", "edit", token.UserID, chapter.ID, map[string]interface{}{
|
||||
"title": input.Title,
|
||||
"source": input.Source,
|
||||
"fictionalDate": fictionalDate,
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/character"
|
||||
)
|
||||
@@ -21,8 +21,8 @@ type CharacterEditInput struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (r *MutationResolver) EditCharacter(ctx context.Context, args struct{ Input
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if character.Author != user.ID && !user.Permitted("character.edit") {
|
||||
if character.Author != token.UserID && !token.Permitted("character.edit") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (r *MutationResolver) EditCharacter(ctx context.Context, args struct{ Input
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Character", "edit", user.ID, character.ID, map[string]interface{}{
|
||||
go change.Submit("Character", "edit", token.UserID, character.ID, map[string]interface{}{
|
||||
"name": character.Name,
|
||||
"shortName": character.ShortName,
|
||||
"description": character.Description,
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/file"
|
||||
)
|
||||
|
||||
@@ -21,8 +21,8 @@ type FileEditArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (r *MutationResolver) EditFile(ctx context.Context, args *FileEditArgs) (*t
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.Author != user.ID && !user.Permitted("file.edit") {
|
||||
if file.Author != token.UserID && !token.Permitted("file.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
)
|
||||
@@ -24,8 +24,8 @@ type LogEditArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("log.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (r *MutationResolver) EditLog(ctx context.Context, args *LogEditArgs) (*typ
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Log", "edit", user.ID, log.ID, map[string]interface{}{
|
||||
go change.Submit("Log", "edit", token.UserID, log.ID, map[string]interface{}{
|
||||
"channel": log.Channel,
|
||||
"title": input.Title,
|
||||
"event": input.Event,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
)
|
||||
@@ -25,8 +25,8 @@ type PostEditArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("post.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (r *MutationResolver) EditPost(ctx context.Context, args *PostEditArgs) (*t
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Post", "edit", user.ID, post.ID, map[string]interface{}{
|
||||
go change.Submit("Post", "edit", token.UserID, post.ID, map[string]interface{}{
|
||||
"time": postTime,
|
||||
"kind": input.Kind,
|
||||
"nick": input.Nick,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
@@ -27,8 +27,8 @@ type StoryEditArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "story.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func (r *MutationResolver) EditStory(ctx context.Context, args *StoryEditArgs) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if story.Author != user.ID && !user.Permitted("story.edit") {
|
||||
if story.Author != token.UserID && !token.Permitted("story.edit") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (r *MutationResolver) EditStory(ctx context.Context, args *StoryEditArgs) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Story", "edit", user.ID, story.ID, map[string]interface{}{
|
||||
go change.Submit("Story", "edit", token.UserID, story.ID, map[string]interface{}{
|
||||
"name": input.Name,
|
||||
"category": input.Category,
|
||||
"author": input.Author,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
)
|
||||
@@ -21,8 +21,8 @@ type PostMoveArgs struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("post.move") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (r *MutationResolver) MovePost(ctx context.Context, args *PostMoveArgs) (*t
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Post", "move", user.ID, post.ID, map[string]interface{}{
|
||||
go change.Submit("Post", "move", token.UserID, post.ID, map[string]interface{}{
|
||||
"logId": post.LogID,
|
||||
"targetIndex": input.ToPosition,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/channel"
|
||||
)
|
||||
@@ -16,8 +16,8 @@ type RemoveChannelArgs struct {
|
||||
|
||||
// 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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("channel.remove") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (r *MutationResolver) RemoveChannel(ctx context.Context, args RemoveChannel
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Channel", "remove", user.ID, channel.Name, nil)
|
||||
go change.Submit("Channel", "remove", token.UserID, channel.Name, nil)
|
||||
|
||||
return &types.ChannelResolver{C: channel}, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
@@ -16,8 +16,8 @@ type RemoveChapterArgs struct {
|
||||
|
||||
// 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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "chapter.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (r *MutationResolver) RemoveChapter(ctx context.Context, args *RemoveChapte
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Chapter", "remove", user.ID, chapter.ID, nil)
|
||||
go change.Submit("Chapter", "remove", token.UserID, chapter.ID, nil)
|
||||
|
||||
return &types.ChapterResolver{C: chapter}, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/character"
|
||||
)
|
||||
@@ -16,8 +16,8 @@ type RemoveCharacterArgs struct {
|
||||
|
||||
// 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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func (r *MutationResolver) RemoveCharacter(ctx context.Context, args RemoveChara
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if character.Author != user.ID && !user.Permitted("character.remove") {
|
||||
if character.Author != token.UserID && !token.Permitted("character.remove") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (r *MutationResolver) RemoveCharacter(ctx context.Context, args RemoveChara
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Character", "remove", user.ID, character.ID, map[string]interface{}{
|
||||
go change.Submit("Character", "remove", token.UserID, character.ID, map[string]interface{}{
|
||||
"name": character.Name,
|
||||
"author": character.Author,
|
||||
"nicks": character.Nicks,
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/character"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
@@ -20,8 +20,8 @@ type RemoveCharacterNick struct {
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func (r *MutationResolver) RemoveCharacterNick(ctx context.Context, args struct{
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if character.Author != user.ID && !user.Permitted("character.edit") {
|
||||
if character.Author != token.UserID && !token.Permitted("character.edit") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func (r *MutationResolver) RemoveCharacterNick(ctx context.Context, args struct{
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Character", "remove.nick", user.ID, character.ID, map[string]interface{}{
|
||||
go change.Submit("Character", "remove.nick", token.UserID, character.ID, map[string]interface{}{
|
||||
"nick": input.Nick,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/file"
|
||||
)
|
||||
|
||||
@@ -15,8 +15,8 @@ type RemoveFileArgs struct {
|
||||
|
||||
// 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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (r *MutationResolver) RemoveFile(ctx context.Context, args *RemoveFileArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file.Author != user.ID && !user.Permitted("file.remove") {
|
||||
if file.Author != token.UserID && !token.Permitted("file.remove") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
)
|
||||
@@ -16,8 +16,8 @@ type RemoveLogArgs struct {
|
||||
|
||||
// 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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("log.remove") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (r *MutationResolver) RemoveLog(ctx context.Context, args *RemoveLogArgs) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Log", "remove", user.ID, log.ID, nil)
|
||||
go change.Submit("Log", "remove", token.UserID, log.ID, nil)
|
||||
|
||||
return &types.LogResolver{L: log}, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/log"
|
||||
)
|
||||
@@ -16,8 +16,8 @@ type PostRemoveArgs struct {
|
||||
|
||||
// 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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("post.remove") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func (r *MutationResolver) RemovePost(ctx context.Context, args PostRemoveArgs)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Post", "remove", user.ID, post.ID, map[string]interface{}{
|
||||
go change.Submit("Post", "remove", token.UserID, post.ID, map[string]interface{}{
|
||||
"logId": post.LogID,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
@@ -16,8 +16,8 @@ type StoryRemoveArgs struct {
|
||||
|
||||
// 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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "story.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func (r *MutationResolver) RemoveStory(ctx context.Context, args *StoryRemoveArg
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if story.Author != user.ID && !user.Permitted("story.remove") {
|
||||
if story.Author != token.UserID && !token.Permitted("story.remove") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func (r *MutationResolver) RemoveStory(ctx context.Context, args *StoryRemoveArg
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Story", "remove", user.ID, story.ID, nil)
|
||||
go change.Submit("Story", "remove", token.UserID, story.ID, nil)
|
||||
|
||||
return &types.StoryResolver{S: story}, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/change"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
@@ -25,8 +25,8 @@ func (r *MutationResolver) RemoveStoryTag(ctx context.Context, args *StoryTagRem
|
||||
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") {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "story.edit") {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func (r *MutationResolver) RemoveStoryTag(ctx context.Context, args *StoryTagRem
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if story.Author != user.ID && !user.Permitted("story.edit") {
|
||||
if story.Author != token.UserID && !token.Permitted("story.edit") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (r *MutationResolver) RemoveStoryTag(ctx context.Context, args *StoryTagRem
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go change.Submit("Story", "remove.tag", user.ID, story.ID, map[string]interface{}{
|
||||
go change.Submit("Story", "remove.tag", token.UserID, story.ID, map[string]interface{}{
|
||||
"kind": tag.Kind,
|
||||
"name": tag.Name,
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ type CharactersArgs struct {
|
||||
Names *[]string
|
||||
Author *string
|
||||
Search *string
|
||||
Logged *bool
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ func (r *QueryResolver) Characters(ctx context.Context, args *CharactersArgs) ([
|
||||
names = *filter.Names
|
||||
}
|
||||
|
||||
characters, err = character.ListFilter(ids, nicks, names, filter.Author, filter.Search)
|
||||
characters, err = character.ListFilter(ids, nicks, names, filter.Author, filter.Search, filter.Logged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/session"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/file"
|
||||
)
|
||||
|
||||
@@ -19,11 +19,11 @@ type FilesArgs struct {
|
||||
// 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()
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
author := ""
|
||||
if user != nil {
|
||||
author = user.ID
|
||||
if token != nil {
|
||||
author = token.UserID
|
||||
}
|
||||
|
||||
mimeTypes := []string(nil)
|
||||
|
||||
@@ -2,71 +2,41 @@ package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/loader"
|
||||
"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
|
||||
}
|
||||
Filter *log.Filter
|
||||
}
|
||||
|
||||
// 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 {
|
||||
filter = log.NewFilter().WithLimit(100)
|
||||
}
|
||||
|
||||
if filter != nil {
|
||||
limit := 100
|
||||
search := ""
|
||||
logs, err := log.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filter.Search != nil {
|
||||
search = *filter.Search
|
||||
limit = 0
|
||||
if len(logs) >= 100 {
|
||||
loader := loader.FromContext(ctx)
|
||||
if loader == nil {
|
||||
return nil, errors.New("no loader")
|
||||
}
|
||||
|
||||
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
|
||||
for _, log := range logs {
|
||||
loader.PrimeCharacters("id", log.CharacterIDs...)
|
||||
loader.PrimeChannels("name", log.Channel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,3 +9,9 @@ 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")
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graphql/resolver/types"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/model/story"
|
||||
)
|
||||
|
||||
@@ -18,52 +19,83 @@ type StoriesArg struct {
|
||||
}
|
||||
EarliestFictionalDate *string
|
||||
LatestFictionalDate *string
|
||||
Category *string
|
||||
Limit *int32
|
||||
Open *bool
|
||||
Unlisted *bool
|
||||
}
|
||||
}
|
||||
|
||||
// Stories resolves the stories query
|
||||
func (r *QueryResolver) Stories(ctx context.Context, args *StoriesArg) ([]*types.StoryResolver, error) {
|
||||
var err error
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
filter := args.Filter
|
||||
|
||||
author := ""
|
||||
if filter != nil && filter.Author != nil {
|
||||
author = *filter.Author
|
||||
}
|
||||
|
||||
category := ""
|
||||
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
|
||||
unlisted := false
|
||||
open := (*bool)(nil)
|
||||
limit := 0
|
||||
|
||||
if filter != nil {
|
||||
if filter.Author != nil {
|
||||
author = *filter.Author
|
||||
}
|
||||
|
||||
if filter.Category != nil {
|
||||
category = *filter.Category
|
||||
}
|
||||
|
||||
if filter.Tags != nil {
|
||||
for _, tagInput := range *filter.Tags {
|
||||
tags = append(tags, story.Tag{
|
||||
Kind: tagInput.Kind,
|
||||
Name: tagInput.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if filter.EarliestFictionalDate != nil {
|
||||
earliest, err = time.Parse(time.RFC3339Nano, *filter.EarliestFictionalDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if filter.LatestFictionalDate != nil {
|
||||
latest, err = time.Parse(time.RFC3339Nano, *filter.LatestFictionalDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if filter.Limit != nil {
|
||||
limit = int(*filter.Limit)
|
||||
}
|
||||
|
||||
unlisted = filter.Unlisted != nil && *filter.Unlisted == true
|
||||
if unlisted {
|
||||
if token == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
if author != "" && author != token.UserID && !token.Permitted("story.unlisted") {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
|
||||
author = token.UserID
|
||||
}
|
||||
|
||||
open = filter.Open
|
||||
}
|
||||
|
||||
limit := 30
|
||||
if filter != nil && filter.Limit != nil {
|
||||
limit = int(*filter.Limit)
|
||||
}
|
||||
|
||||
stories, err := story.List(author, tags, earliest, latest, limit)
|
||||
stories, err := story.List(author, category, tags, earliest, latest, unlisted, open, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package types
|
||||
|
||||
import "git.aiterp.net/rpdata/api/internal/session"
|
||||
|
||||
// UserResolver resulves the user type
|
||||
type UserResolver struct{ U *session.User }
|
||||
|
||||
// ID resolves User.id
|
||||
func (r *UserResolver) ID() string {
|
||||
return r.U.ID
|
||||
}
|
||||
|
||||
// Permissions resolves User.permissions
|
||||
func (r *UserResolver) Permissions() []string {
|
||||
return r.U.Permissions
|
||||
}
|
||||
@@ -49,10 +49,6 @@ type Query {
|
||||
|
||||
# Find all distinct tags used in stories
|
||||
tags: [Tag!]!
|
||||
|
||||
|
||||
# Find current session
|
||||
session: Session!
|
||||
}
|
||||
|
||||
# The Mutation type represents write entry points into the API.
|
||||
@@ -137,13 +133,6 @@ type Mutation {
|
||||
|
||||
# Remove a chapter
|
||||
removeChapter(id: String!): Chapter!
|
||||
|
||||
|
||||
# Log in
|
||||
login(username: String!, password: String!): Session!
|
||||
|
||||
# Log out
|
||||
logout(): Session!
|
||||
}
|
||||
|
||||
schema {
|
||||
|
||||
@@ -38,6 +38,9 @@ input CharactersFilter {
|
||||
|
||||
# Filter by text search matching against the character's description
|
||||
search: String
|
||||
|
||||
# Filter by whether they've been part of a log.
|
||||
logged: Boolean
|
||||
}
|
||||
|
||||
# Input for adding characters
|
||||
|
||||
@@ -50,10 +50,10 @@ input LogsFilter {
|
||||
# Search post content
|
||||
search: String
|
||||
|
||||
# Limit to only open logs
|
||||
# Limit by whether it's open or not
|
||||
open: Boolean
|
||||
|
||||
# Limit the amount of posts
|
||||
# Limit the amount of results you get back
|
||||
limit: Int
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# The session represents the current login state
|
||||
type Session {
|
||||
# The user that is logged in, null if not logged in
|
||||
user: User
|
||||
}
|
||||
@@ -40,9 +40,18 @@ input StoriesFilter {
|
||||
# What author to query for
|
||||
author: String
|
||||
|
||||
# What category to query for
|
||||
category: StoryCategory
|
||||
|
||||
# What tags to query for
|
||||
tags: [TagInput!]
|
||||
|
||||
# If true, it will only show unlisted page you have access to
|
||||
unlisted: Boolean
|
||||
|
||||
# Whether the page is open for additions by other users
|
||||
open: Boolean
|
||||
|
||||
# The earliest fictionalDate
|
||||
earliestFictionalDate: String
|
||||
|
||||
@@ -125,18 +134,18 @@ input StoryTagRemoveInput {
|
||||
|
||||
# Possible values for Story.category
|
||||
enum StoryCategory {
|
||||
# Description and content of a document or item
|
||||
Document
|
||||
# General information
|
||||
Info
|
||||
|
||||
# News stories
|
||||
News
|
||||
|
||||
# Description and content of a document or item
|
||||
Document
|
||||
|
||||
# Information about something going on in the background that may or may not inform RP
|
||||
Background
|
||||
|
||||
# General information
|
||||
Info
|
||||
|
||||
# A short story
|
||||
Story
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
var keyCollection *mgo.Collection
|
||||
|
||||
// A Key contains a JWT secret and the limitations of it. There are two types of
|
||||
// keys, single-user keys and wildcard keys. The former is used to authenticate
|
||||
// a single user (e.g. the logbot) through an API while the latter is only for
|
||||
// services that can be trusted to perform its own authentication (a frontend).
|
||||
type Key struct {
|
||||
ID string `bson:"_id"`
|
||||
Name string `bson:"name"`
|
||||
User string `bson:"user"`
|
||||
Secret string `bson:"secret"`
|
||||
}
|
||||
|
||||
// ValidForUser returns true if the key's user is the same as
|
||||
// the user, or it's a wildcard key.
|
||||
func (key *Key) ValidForUser(user string) bool {
|
||||
return key.User == user || key.User == "*"
|
||||
}
|
||||
|
||||
// FindKey finds a key by kid (key ID)
|
||||
func FindKey(kid string) (Key, error) {
|
||||
key := Key{}
|
||||
err := keyCollection.FindId(kid).One(&key)
|
||||
|
||||
return key, err
|
||||
}
|
||||
|
||||
// NewKey generates a new key for the user and name. This
|
||||
// does not allow generating wildcard keys, they have to be
|
||||
// manually inserted into the DB.
|
||||
func NewKey(name, user string) (*Key, error) {
|
||||
if user == "*" {
|
||||
return nil, errors.New("auth: wildcard keys not allowed")
|
||||
}
|
||||
|
||||
secret, err := makeKeySecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := &Key{
|
||||
ID: makeKeyID(),
|
||||
Name: name,
|
||||
User: user,
|
||||
Secret: secret,
|
||||
}
|
||||
|
||||
if err := keyCollection.Insert(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
keyCollection = db.C("auth.keys")
|
||||
|
||||
keyCollection.EnsureIndexKey("user")
|
||||
})
|
||||
}
|
||||
|
||||
// makeKeyID makes a random story ID that's 16 characters long
|
||||
func makeKeyID() string {
|
||||
result := "K"
|
||||
offset := 0
|
||||
data := make([]byte, 32)
|
||||
|
||||
rand.Read(data)
|
||||
for len(result) < 16 {
|
||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
||||
offset += 8
|
||||
|
||||
if offset >= 32 {
|
||||
rand.Read(data)
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
return result[:16]
|
||||
}
|
||||
|
||||
func makeKeySecret() (string, error) {
|
||||
data := make([]byte, 64)
|
||||
alphabet := "0123456789abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSUTVWXYZ-_"
|
||||
|
||||
_, err := rand.Read(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := range data {
|
||||
data[i] = alphabet[data[i]%64]
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -1,16 +1,4 @@
|
||||
package session
|
||||
|
||||
// DefaultPermissions gets the default permissions
|
||||
func DefaultPermissions() []string {
|
||||
return []string{
|
||||
"member",
|
||||
"log.edit",
|
||||
"log.reorder",
|
||||
"post.edit",
|
||||
"post.move",
|
||||
"file.upload",
|
||||
}
|
||||
}
|
||||
package auth
|
||||
|
||||
// AllPermissions gets all permissions and their purpose
|
||||
func AllPermissions() map[string]string {
|
||||
@@ -30,8 +18,10 @@ func AllPermissions() map[string]string {
|
||||
"post.edit": "Can edit posts",
|
||||
"post.move": "Can move posts",
|
||||
"post.remove": "Can remove posts",
|
||||
"story.add": "Can add non-owned stories",
|
||||
"story.edit": "Can edit non-owned stories",
|
||||
"story.remove": "Can remove non-owned stories",
|
||||
"story.unlisted": "Can view non-owned unlisted stories",
|
||||
"file.upload": "Can upload files",
|
||||
"file.edit": "Can edit non-owned files",
|
||||
"file.remove": "Can remove non-owned files",
|
||||
@@ -0,0 +1,154 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/users"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
var contextKey = &struct{ data string }{"Token Context Key"}
|
||||
|
||||
// ErrNoKid is returned if the key id is missing from the jwt token header,
|
||||
var ErrNoKid = errors.New("Missing \"kid\" field in token")
|
||||
|
||||
// ErrKeyNotFound is returned if the key wasn't found.
|
||||
var ErrKeyNotFound = errors.New("Key not found")
|
||||
|
||||
// ErrInvalidClaims is returned by parseClaims if the claims cannot be parsed
|
||||
var ErrInvalidClaims = errors.New("Invalid claims in token")
|
||||
|
||||
// ErrExpired is returned by parseClaims if the expiry date is in the past
|
||||
var ErrExpired = errors.New("Claims have already expired")
|
||||
|
||||
// ErrWrongUser is returned by CheckToken if the key cannot represent this user
|
||||
var ErrWrongUser = errors.New("Key is not valid for this user")
|
||||
|
||||
// ErrWrongPermissions is returned by CheckToken if the key cannot claim one or more of its permissions
|
||||
var ErrWrongPermissions = errors.New("User does not have these permissions")
|
||||
|
||||
// ErrDeletedUser is returned by CheckToken if the key can represent this user, but the user doesn't exist.
|
||||
var ErrDeletedUser = errors.New("User was not found")
|
||||
|
||||
// TokenFromContext gets the token from context.
|
||||
func TokenFromContext(ctx context.Context) *models.Token {
|
||||
token, ok := ctx.Value(contextKey).(*models.Token)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// RequestWithToken either returns the request, or the request with a new context that
|
||||
// has the token.
|
||||
func RequestWithToken(r *http.Request) *http.Request {
|
||||
header := r.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return r
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(header, "Bearer ") {
|
||||
return r
|
||||
}
|
||||
|
||||
token, err := CheckToken(header[7:])
|
||||
if err != nil {
|
||||
return r
|
||||
}
|
||||
|
||||
return r.WithContext(context.WithValue(r.Context(), contextKey, &token))
|
||||
}
|
||||
|
||||
// CheckToken reads the token string and returns a token if everything is kosher.
|
||||
func CheckToken(tokenString string) (token models.Token, err error) {
|
||||
var key Key
|
||||
|
||||
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
|
||||
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
|
||||
}
|
||||
|
||||
kid, ok := jwtToken.Header["kid"].(string)
|
||||
if !ok {
|
||||
return nil, ErrNoKid
|
||||
}
|
||||
|
||||
key, err = FindKey(kid)
|
||||
if err != nil || key.ID == "" {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
|
||||
return []byte(key.Secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return models.Token{}, err
|
||||
}
|
||||
|
||||
userid, permissions, err := parseClaims(jwtToken.Claims)
|
||||
if err != nil {
|
||||
return models.Token{}, err
|
||||
}
|
||||
|
||||
if !key.ValidForUser(userid) {
|
||||
return models.Token{}, ErrWrongUser
|
||||
}
|
||||
|
||||
user, err := users.Ensure(userid)
|
||||
if err != nil {
|
||||
return models.Token{}, ErrDeletedUser
|
||||
}
|
||||
|
||||
acceptedPermissions := make([]string, 0, 8)
|
||||
for _, permission := range permissions {
|
||||
found := false
|
||||
|
||||
for _, userPermission := range user.Permissions {
|
||||
if permission == userPermission {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
acceptedPermissions = append(acceptedPermissions, permission)
|
||||
}
|
||||
}
|
||||
|
||||
return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil
|
||||
}
|
||||
|
||||
func parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err error) {
|
||||
mapClaims, ok := jwtClaims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return "", nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
if !mapClaims.VerifyExpiresAt(time.Now().Unix(), true) {
|
||||
return "", nil, ErrExpired
|
||||
}
|
||||
|
||||
if userid, ok = mapClaims["user"].(string); !ok {
|
||||
return "", nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
if claimedPermissions, ok := mapClaims["permissions"].([]interface{}); ok {
|
||||
for _, permission := range claimedPermissions {
|
||||
if permission, ok := permission.(string); ok {
|
||||
permissions = append(permissions, permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(permissions) == 0 {
|
||||
return "", nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -5,27 +5,30 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/model/channel"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/channels"
|
||||
"github.com/graph-gophers/dataloader"
|
||||
)
|
||||
|
||||
// Channel gets a character by key
|
||||
func (loader *Loader) Channel(key, value string) (channel.Channel, error) {
|
||||
func (loader *Loader) Channel(key, value string) (models.Channel, error) {
|
||||
if !strings.HasPrefix(key, "Channel.") {
|
||||
key = "Channel." + key
|
||||
}
|
||||
|
||||
if loader.loaders[key] == nil {
|
||||
return channel.Channel{}, errors.New("unsupported key")
|
||||
return models.Channel{}, errors.New("unsupported key")
|
||||
}
|
||||
|
||||
loader.loadPrimed(key)
|
||||
|
||||
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
|
||||
res, err := thunk()
|
||||
if err != nil {
|
||||
return channel.Channel{}, err
|
||||
return models.Channel{}, err
|
||||
}
|
||||
|
||||
channel, ok := res.(channel.Channel)
|
||||
channel, ok := res.(models.Channel)
|
||||
if !ok {
|
||||
return channel, errors.New("incorrect type")
|
||||
}
|
||||
@@ -33,14 +36,23 @@ func (loader *Loader) Channel(key, value string) (channel.Channel, error) {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// PrimeChannels primes channels for loading along with the first one.
|
||||
func (loader *Loader) PrimeChannels(key string, values ...string) {
|
||||
if !strings.HasPrefix(key, "Channel.") {
|
||||
key = "Channel." + key
|
||||
}
|
||||
|
||||
loader.prime(key, values)
|
||||
}
|
||||
|
||||
func channelNameBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
var results []*dataloader.Result
|
||||
names := keys.Keys()
|
||||
|
||||
channels, err := channel.ListNames(names...)
|
||||
channels, err := channels.ListNames(names...)
|
||||
if err != nil {
|
||||
for range names {
|
||||
results = append(results, &dataloader.Result{Data: channel.Channel{}, Error: err})
|
||||
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
|
||||
}
|
||||
|
||||
return results
|
||||
@@ -58,7 +70,7 @@ func channelNameBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.R
|
||||
}
|
||||
|
||||
if !found {
|
||||
results = append(results, &dataloader.Result{Data: channel.Channel{}, Error: err})
|
||||
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,39 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/model/character"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/characters"
|
||||
"github.com/graph-gophers/dataloader"
|
||||
)
|
||||
|
||||
// Character gets a character by key
|
||||
func (loader *Loader) Character(key, value string) (character.Character, error) {
|
||||
func (loader *Loader) Character(key, value string) (models.Character, error) {
|
||||
if !strings.HasPrefix(key, "Character.") {
|
||||
key = "Character." + key
|
||||
}
|
||||
|
||||
loader.loadPrimed(key)
|
||||
|
||||
if loader.loaders[key] == nil {
|
||||
return character.Character{}, errors.New("unsupported key")
|
||||
return models.Character{}, errors.New("unsupported key")
|
||||
}
|
||||
|
||||
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
|
||||
res, err := thunk()
|
||||
if err != nil {
|
||||
return character.Character{}, err
|
||||
return models.Character{}, err
|
||||
}
|
||||
|
||||
char, ok := res.(character.Character)
|
||||
char, ok := res.(models.Character)
|
||||
if !ok {
|
||||
return character.Character{}, errors.New("incorrect type")
|
||||
return models.Character{}, errors.New("incorrect type")
|
||||
}
|
||||
|
||||
return char, nil
|
||||
}
|
||||
|
||||
// Characters gets characters by key
|
||||
func (loader *Loader) Characters(key string, values ...string) ([]character.Character, error) {
|
||||
func (loader *Loader) Characters(key string, values ...string) ([]models.Character, error) {
|
||||
if !strings.HasPrefix(key, "Character.") {
|
||||
key = "Character." + key
|
||||
}
|
||||
@@ -43,6 +46,8 @@ func (loader *Loader) Characters(key string, values ...string) ([]character.Char
|
||||
return nil, errors.New("unsupported key")
|
||||
}
|
||||
|
||||
loader.loadPrimed(key)
|
||||
|
||||
thunk := loader.loaders[key].LoadMany(loader.ctx, dataloader.NewKeysFromStrings(values))
|
||||
res, errs := thunk()
|
||||
for _, err := range errs {
|
||||
@@ -51,10 +56,10 @@ func (loader *Loader) Characters(key string, values ...string) ([]character.Char
|
||||
}
|
||||
}
|
||||
|
||||
chars := make([]character.Character, len(res))
|
||||
chars := make([]models.Character, len(res))
|
||||
|
||||
for i := range res {
|
||||
char, ok := res[i].(character.Character)
|
||||
char, ok := res[i].(models.Character)
|
||||
if !ok {
|
||||
return nil, errors.New("incorrect type")
|
||||
}
|
||||
@@ -65,11 +70,22 @@ func (loader *Loader) Characters(key string, values ...string) ([]character.Char
|
||||
return chars, nil
|
||||
}
|
||||
|
||||
// PrimeCharacters adds a set of characters to be loaded if, and only if, characters
|
||||
// are going to be loaded. This will fill up the cache and speed up subsequent dataloader
|
||||
// runs.
|
||||
func (loader *Loader) PrimeCharacters(key string, values ...string) {
|
||||
if !strings.HasPrefix(key, "Character.") {
|
||||
key = "Character." + key
|
||||
}
|
||||
|
||||
loader.prime(key, values)
|
||||
}
|
||||
|
||||
func characterIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
results := make([]*dataloader.Result, 0, len(keys))
|
||||
ids := keys.Keys()
|
||||
|
||||
characters, err := character.ListIDs(ids...)
|
||||
characters, err := characters.List(&characters.Filter{IDs: ids})
|
||||
if err != nil {
|
||||
for range ids {
|
||||
results = append(results, &dataloader.Result{Error: err})
|
||||
@@ -90,7 +106,7 @@ func characterIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.R
|
||||
}
|
||||
|
||||
if !found {
|
||||
results = append(results, &dataloader.Result{Data: character.Character{}, Error: ErrNotFound})
|
||||
results = append(results, &dataloader.Result{Data: models.Character{}, Error: ErrNotFound})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +117,7 @@ func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader
|
||||
var results []*dataloader.Result
|
||||
nicks := keys.Keys()
|
||||
|
||||
characters, err := character.ListNicks(nicks...)
|
||||
characters, err := characters.List(&characters.Filter{Nicks: nicks})
|
||||
if err != nil {
|
||||
for range nicks {
|
||||
results = append(results, &dataloader.Result{Error: err})
|
||||
@@ -122,7 +138,7 @@ func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader
|
||||
}
|
||||
|
||||
if !found {
|
||||
results = append(results, &dataloader.Result{Data: character.Character{}, Error: err})
|
||||
results = append(results, &dataloader.Result{Data: models.Character{}, Error: err})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package loader
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/graph-gophers/dataloader"
|
||||
@@ -16,8 +17,11 @@ var ErrNotFound = errors.New("not found")
|
||||
// A Loader is a collection of data loaders and functions to act on them. It's supposed to be
|
||||
// request-scoped, and will thus keep things cached indefinitely.
|
||||
type Loader struct {
|
||||
mutex sync.Mutex
|
||||
ctx context.Context
|
||||
loaders map[string]*dataloader.Loader
|
||||
|
||||
primedKeys map[string]map[string]bool
|
||||
}
|
||||
|
||||
// New initializes the loader.
|
||||
@@ -25,10 +29,11 @@ func New() *Loader {
|
||||
return &Loader{
|
||||
ctx: context.Background(),
|
||||
loaders: map[string]*dataloader.Loader{
|
||||
"Character.id": dataloader.NewBatchedLoader(characterIDBatch, dataloader.WithWait(time.Millisecond)),
|
||||
"Character.nick": dataloader.NewBatchedLoader(characterNickBatch, dataloader.WithWait(time.Millisecond)),
|
||||
"Channel.name": dataloader.NewBatchedLoader(channelNameBatch, dataloader.WithWait(time.Millisecond)),
|
||||
"Character.id": dataloader.NewBatchedLoader(characterIDBatch, dataloader.WithWait(time.Millisecond*2)),
|
||||
"Character.nick": dataloader.NewBatchedLoader(characterNickBatch, dataloader.WithWait(time.Millisecond*2)),
|
||||
"Channel.name": dataloader.NewBatchedLoader(channelNameBatch, dataloader.WithWait(time.Millisecond*2)),
|
||||
},
|
||||
primedKeys: make(map[string]map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,3 +52,29 @@ func (loader *Loader) ToContext(ctx context.Context) context.Context {
|
||||
loader.ctx = ctx
|
||||
return context.WithValue(ctx, &contextKey, loader)
|
||||
}
|
||||
|
||||
func (loader *Loader) prime(key string, values []string) {
|
||||
loader.mutex.Lock()
|
||||
if loader.primedKeys[key] == nil {
|
||||
loader.primedKeys[key] = make(map[string]bool)
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
loader.primedKeys[key][value] = true
|
||||
}
|
||||
loader.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (loader *Loader) loadPrimed(key string) {
|
||||
loader.mutex.Lock()
|
||||
if len(loader.primedKeys[key]) > 0 {
|
||||
primedKeys := make([]string, 0, len(loader.primedKeys[key]))
|
||||
for key := range loader.primedKeys[key] {
|
||||
primedKeys = append(primedKeys, key)
|
||||
}
|
||||
|
||||
loader.loaders[key].LoadMany(loader.ctx, dataloader.NewKeysFromStrings(primedKeys))
|
||||
loader.primedKeys[key] = nil
|
||||
}
|
||||
loader.mutex.Unlock()
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package session
|
||||
|
||||
import "context"
|
||||
|
||||
type contextKeyType struct{ name string }
|
||||
|
||||
func (ck *contextKeyType) String() string {
|
||||
return ck.name
|
||||
}
|
||||
|
||||
var contextKey = &contextKeyType{name: "session context key"}
|
||||
|
||||
// FromContext gets a session fron the context.
|
||||
func FromContext(ctx context.Context) *Session {
|
||||
return ctx.Value(contextKey).(*Session)
|
||||
}
|
||||
|
||||
func contextWithSession(parent context.Context, session *Session) context.Context {
|
||||
return context.WithValue(parent, contextKey, session)
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/aiterp/wikiauth"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/config"
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var sessionCollection *mgo.Collection
|
||||
|
||||
// A Session represents a login session.
|
||||
type Session struct {
|
||||
mutex sync.Mutex
|
||||
|
||||
ID string `bson:"_id"`
|
||||
Time time.Time `bson:"time"`
|
||||
UserID string `bson:"userId"`
|
||||
|
||||
user *User
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
// Load loads a session from a cookie, returning either `r` or a request
|
||||
// with the session context.
|
||||
func Load(w http.ResponseWriter, r *http.Request) *http.Request {
|
||||
cookie, err := r.Cookie("aiterp_session")
|
||||
if err != nil {
|
||||
return r.WithContext(contextWithSession(r.Context(), &Session{w: w}))
|
||||
}
|
||||
|
||||
id := cookie.Value
|
||||
|
||||
session := Session{}
|
||||
err = sessionCollection.FindId(id).One(&session)
|
||||
if err != nil || time.Since(session.Time) > time.Hour*168 {
|
||||
return r.WithContext(contextWithSession(r.Context(), &Session{w: w}))
|
||||
}
|
||||
|
||||
if session.ID != "" && time.Since(session.Time) > time.Second*30 {
|
||||
session.Time = time.Now()
|
||||
go sessionCollection.UpdateId(id, bson.M{"$set": bson.M{"time": session.Time}})
|
||||
}
|
||||
|
||||
cookie.Expires = time.Now().Add(time.Hour * 168)
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
session.w = w
|
||||
|
||||
return r.WithContext(contextWithSession(r.Context(), &session))
|
||||
}
|
||||
|
||||
// Login logs a user in.
|
||||
func (session *Session) Login(username, password string) error {
|
||||
auth := wikiauth.New(config.Global().Wiki.URL)
|
||||
|
||||
err := auth.Login(username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Allow bot passwords
|
||||
username = strings.SplitN(username, "@", 2)[0]
|
||||
|
||||
data := make([]byte, 32)
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.ID = hex.EncodeToString(data)
|
||||
session.UserID = username
|
||||
session.Time = time.Now()
|
||||
|
||||
err = sessionCollection.Insert(&session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.SetCookie(session.w, &http.Cookie{
|
||||
Name: "aiterp_session",
|
||||
Value: session.ID,
|
||||
Expires: time.Now().Add(time.Hour * 2160), // 90 days
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
user, err := FindUser(session.UserID)
|
||||
if err == mgo.ErrNotFound {
|
||||
user = User{ID: username, Nick: "", Permissions: DefaultPermissions()}
|
||||
|
||||
err := userCollection.Insert(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout logs out the session
|
||||
func (session *Session) Logout() {
|
||||
http.SetCookie(session.w, &http.Cookie{
|
||||
Name: "aiterp_session",
|
||||
Value: "",
|
||||
Expires: time.Unix(0, 0),
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
session.mutex.Lock()
|
||||
session.user = nil
|
||||
session.UserID = ""
|
||||
session.ID = ""
|
||||
session.mutex.Unlock()
|
||||
|
||||
sessionCollection.RemoveId(session.ID)
|
||||
}
|
||||
|
||||
// User gets the user information for the session.
|
||||
func (session *Session) User() *User {
|
||||
session.mutex.Lock()
|
||||
defer session.mutex.Unlock()
|
||||
|
||||
if session.user != nil {
|
||||
return session.user
|
||||
}
|
||||
|
||||
if session.UserID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := FindUser(session.UserID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &user
|
||||
}
|
||||
|
||||
// NameOrPermitted is a shorthand for checking the username OR permissions, e.g. to check
|
||||
// if a logged in user can edit a certain post.
|
||||
func (session *Session) NameOrPermitted(userid string, permissions ...string) bool {
|
||||
if session.UserID == userid {
|
||||
return true
|
||||
}
|
||||
|
||||
user := session.User()
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return user.Permitted()
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
sessionCollection = db.C("core.sessions")
|
||||
|
||||
sessionCollection.EnsureIndexKey("nick")
|
||||
sessionCollection.EnsureIndexKey("userId")
|
||||
|
||||
err := sessionCollection.EnsureIndex(mgo.Index{
|
||||
Name: "time",
|
||||
Key: []string{"time"},
|
||||
ExpireAfter: time.Hour * 168,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var globalCtx, globalCancel = context.WithCancel(context.Background())
|
||||
|
||||
// A Task is a wrapper around a function that offers scheduling
|
||||
// and syncronization.
|
||||
type Task struct {
|
||||
mutex sync.Mutex
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
waitDuration time.Duration
|
||||
scheduled bool
|
||||
callback func() error
|
||||
}
|
||||
|
||||
// Context gets the task's context.
|
||||
func (task *Task) Context() context.Context {
|
||||
return task.ctx
|
||||
}
|
||||
|
||||
// Stop stops a task. This should not be done to stop a single run,
|
||||
// but as a way to cancel it forever.
|
||||
func (task *Task) Stop() {
|
||||
task.ctxCancel()
|
||||
}
|
||||
|
||||
// Schedule schedules a task for later execution.
|
||||
func (task *Task) Schedule() {
|
||||
// Don't if the context is closed.
|
||||
select {
|
||||
case <-task.Context().Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
task.mutex.Lock()
|
||||
if task.scheduled {
|
||||
task.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
task.scheduled = true
|
||||
task.mutex.Unlock()
|
||||
|
||||
go func() {
|
||||
<-time.After(task.waitDuration)
|
||||
|
||||
// Mark as no longer scheduled
|
||||
task.mutex.Lock()
|
||||
task.scheduled = false
|
||||
task.mutex.Unlock()
|
||||
|
||||
// Don't if the task is cancelled
|
||||
select {
|
||||
case <-task.Context().Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
task.callback()
|
||||
}()
|
||||
}
|
||||
|
||||
// New makes a new task with the callback.
|
||||
func New(waitDuration time.Duration, callback func() error) *Task {
|
||||
ctx, ctxCancel := context.WithCancel(globalCtx)
|
||||
|
||||
return &Task{
|
||||
callback: callback,
|
||||
ctx: ctx,
|
||||
ctxCancel: ctxCancel,
|
||||
waitDuration: waitDuration,
|
||||
}
|
||||
}
|
||||
|
||||
// StopAll stops all tasks.
|
||||
func StopAll() {
|
||||
globalCancel()
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package channel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var collection *mgo.Collection
|
||||
|
||||
// ErrInvalidName is an error for an invalid channel name
|
||||
var ErrInvalidName = errors.New("Invalid channel name")
|
||||
|
||||
// A Channel represents information abount an IRC RP channel, and whether it should be logged
|
||||
type Channel struct {
|
||||
Name string `bson:"_id"`
|
||||
Logged bool `bson:"logged"`
|
||||
Hub bool `bson:"hub"`
|
||||
Event string `bson:"event,omitempty"`
|
||||
Location string `bson:"location,omitempty"`
|
||||
}
|
||||
|
||||
// Edit edits the channel
|
||||
func (channel *Channel) Edit(logged, hub *bool, event, location *string) error {
|
||||
changes := bson.M{}
|
||||
changed := *channel
|
||||
|
||||
if logged != nil && channel.Logged != *logged {
|
||||
changes["logged"] = *logged
|
||||
changed.Logged = *logged
|
||||
}
|
||||
if hub != nil && channel.Hub != *hub {
|
||||
changes["hub"] = *hub
|
||||
changed.Hub = *hub
|
||||
}
|
||||
if event != nil && channel.Event != *event {
|
||||
changes["event"] = *event
|
||||
changed.Event = *event
|
||||
}
|
||||
if location != nil && channel.Event != *location {
|
||||
changes["location"] = *location
|
||||
changed.Location = *location
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := collection.UpdateId(channel.Name, bson.M{"$set": changes})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*channel = changed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes the channel information from the database.
|
||||
func (channel *Channel) Remove() error {
|
||||
return collection.RemoveId(channel.Name)
|
||||
}
|
||||
|
||||
// Ensure ensures a channel's existence. It does not change `logged` if there is
|
||||
// an existing channel.
|
||||
func Ensure(name string, logged bool) (Channel, error) {
|
||||
channel, err := FindName(name)
|
||||
if err == mgo.ErrNotFound {
|
||||
return New(name, logged, false, "", "")
|
||||
} else if err != nil {
|
||||
return Channel{}, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// New creates a new channel
|
||||
func New(name string, logged, hub bool, event, location string) (Channel, error) {
|
||||
if len(name) < 3 && !strings.HasPrefix(name, "#") {
|
||||
return Channel{}, ErrInvalidName
|
||||
}
|
||||
|
||||
channel := Channel{
|
||||
Name: name,
|
||||
Logged: logged,
|
||||
Hub: hub,
|
||||
Event: event,
|
||||
Location: location,
|
||||
}
|
||||
|
||||
err := collection.Insert(channel)
|
||||
if err != nil {
|
||||
return Channel{}, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// FindName finds a channel by its id (its name).
|
||||
func FindName(name string) (Channel, error) {
|
||||
channel := Channel{}
|
||||
err := collection.FindId(name).One(&channel)
|
||||
|
||||
return channel, err
|
||||
}
|
||||
|
||||
// List finds channels, if logged is true it will be limited to logged
|
||||
// channels
|
||||
func List(logged *bool, eventName *string, locationName *string) ([]Channel, error) {
|
||||
query := bson.M{}
|
||||
|
||||
if logged != nil {
|
||||
query["logged"] = *logged
|
||||
}
|
||||
if eventName != nil {
|
||||
query["eventName"] = *eventName
|
||||
}
|
||||
if locationName != nil {
|
||||
query["locationName"] = *locationName
|
||||
}
|
||||
|
||||
channels := make([]Channel, 0, 128)
|
||||
err := collection.Find(query).All(&channels)
|
||||
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// ListNames finds channels by the names provided
|
||||
func ListNames(names ...string) ([]Channel, error) {
|
||||
query := bson.M{"_id": bson.M{"$in": names}}
|
||||
|
||||
channels := make([]Channel, 0, 32)
|
||||
err := collection.Find(query).All(&channels)
|
||||
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
collection = db.C("common.channels")
|
||||
|
||||
collection.EnsureIndexKey("logged")
|
||||
collection.EnsureIndexKey("hub")
|
||||
collection.EnsureIndexKey("event")
|
||||
collection.EnsureIndexKey("location")
|
||||
})
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
package character
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/model/counter"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var collection *mgo.Collection
|
||||
|
||||
// Character is a common data model representing an RP character or NPC.
|
||||
type Character struct {
|
||||
ID string `json:"id" bson:"_id"`
|
||||
Nicks []string `json:"nicks" bson:"nicks"`
|
||||
Name string `json:"name" bson:"name"`
|
||||
ShortName string `json:"shortName" bson:"shortName"`
|
||||
Author string `json:"author" bson:"author"`
|
||||
Description string `json:"description" bson:"description"`
|
||||
}
|
||||
|
||||
// HasNick returns true if the character has that nick
|
||||
func (character *Character) HasNick(nick string) bool {
|
||||
for i := range character.Nicks {
|
||||
if strings.EqualFold(character.Nicks[i], nick) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AddNick adds a nick to the character. It will return an error
|
||||
// if the nick already exists.
|
||||
func (character *Character) AddNick(nick string) error {
|
||||
for i := range character.Nicks {
|
||||
if strings.EqualFold(character.Nicks[i], nick) {
|
||||
return errors.New("Nick already exists")
|
||||
}
|
||||
}
|
||||
|
||||
err := collection.UpdateId(character.ID, bson.M{"$push": bson.M{"nicks": nick}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
character.Nicks = append(character.Nicks, nick)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNick removes the nick from the character. It will raise
|
||||
// an error if the nick does not exist; even if that kind of is
|
||||
// the end goal.
|
||||
func (character *Character) RemoveNick(nick string) error {
|
||||
index := -1
|
||||
for i := range character.Nicks {
|
||||
if strings.EqualFold(character.Nicks[i], nick) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return errors.New("Nick does not exist")
|
||||
}
|
||||
|
||||
err := collection.UpdateId(character.ID, bson.M{"$pull": bson.M{"nicks": nick}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
character.Nicks = append(character.Nicks[:index], character.Nicks[index+1:]...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edit sets the fields of metadata. Only non-empty and different fields will be set in the
|
||||
// database, preventing out of order edits to two fields from conflicting
|
||||
func (character *Character) Edit(name, shortName, description string) error {
|
||||
changes := bson.M{}
|
||||
if len(name) > 0 && name != character.Name {
|
||||
changes["name"] = name
|
||||
}
|
||||
if len(shortName) > 0 && shortName != character.ShortName {
|
||||
changes["shortName"] = shortName
|
||||
}
|
||||
if len(description) > 0 && description != character.Description {
|
||||
changes["description"] = description
|
||||
}
|
||||
|
||||
err := collection.UpdateId(character.ID, changes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if changes["name"] != nil {
|
||||
character.Name = name
|
||||
}
|
||||
if changes["shortName"] != nil {
|
||||
character.ShortName = shortName
|
||||
}
|
||||
if changes["description"] != nil {
|
||||
character.Description = description
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes the character from the database. The reason this is an instance method
|
||||
// is that it should only be done after an authorization check.
|
||||
func (character *Character) Remove() error {
|
||||
return collection.RemoveId(character.ID)
|
||||
}
|
||||
|
||||
// FindID finds Character by ID
|
||||
func FindID(id string) (Character, error) {
|
||||
return find(bson.M{"_id": id})
|
||||
}
|
||||
|
||||
// FindNick finds Character by nick
|
||||
func FindNick(nick string) (Character, error) {
|
||||
return find(bson.M{"nicks": nick})
|
||||
}
|
||||
|
||||
// FindName finds Character by either full name or
|
||||
// short name.
|
||||
func FindName(name string) (Character, error) {
|
||||
return find(bson.M{"$or": []bson.M{bson.M{"name": name}, bson.M{"shortName": name}}})
|
||||
}
|
||||
|
||||
// List lists all characters
|
||||
func List() ([]Character, error) {
|
||||
return list(bson.M{})
|
||||
}
|
||||
|
||||
// ListAuthor lists all characters by author
|
||||
func ListAuthor(author string) ([]Character, error) {
|
||||
return list(bson.M{"author": author})
|
||||
}
|
||||
|
||||
// ListNicks lists all characters with either of these nicks. This was made with
|
||||
// the logbot in mind, to batch an order for characters.
|
||||
func ListNicks(nicks ...string) ([]Character, error) {
|
||||
return list(bson.M{"nicks": bson.M{"$in": nicks}})
|
||||
}
|
||||
|
||||
// ListIDs lists all characters with either of these IDs.
|
||||
func ListIDs(ids ...string) ([]Character, error) {
|
||||
return list(bson.M{"_id": bson.M{"$in": ids}})
|
||||
}
|
||||
|
||||
// ListFilter lists all logs matching the filters.
|
||||
func ListFilter(ids []string, nicks []string, names []string, author *string, search *string) ([]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.
|
||||
func New(nick, name, shortName, author, description string) (Character, error) {
|
||||
if len(nick) < 1 || len(name) < 1 || len(author) < 1 {
|
||||
return Character{}, errors.New("Nick, name, or author name too short or empty")
|
||||
}
|
||||
if shortName == "" {
|
||||
shortName = strings.SplitN(name, " ", 2)[0]
|
||||
}
|
||||
|
||||
char, err := FindNick(nick)
|
||||
if err == nil && char.ID != "" {
|
||||
return Character{}, errors.New("Nick is occupied")
|
||||
}
|
||||
|
||||
nextID, err := counter.Next("auto_increment", "Character")
|
||||
if err != nil {
|
||||
return Character{}, err
|
||||
}
|
||||
|
||||
character := Character{
|
||||
ID: "C" + strconv.Itoa(nextID),
|
||||
Nicks: []string{nick},
|
||||
Name: name,
|
||||
ShortName: shortName,
|
||||
Author: author,
|
||||
Description: description,
|
||||
}
|
||||
|
||||
err = collection.Insert(character)
|
||||
if err != nil {
|
||||
return Character{}, err
|
||||
}
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func find(query interface{}) (Character, error) {
|
||||
character := Character{}
|
||||
err := collection.Find(query).One(&character)
|
||||
if err != nil {
|
||||
return Character{}, err
|
||||
}
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func list(query interface{}) ([]Character, error) {
|
||||
characters := make([]Character, 0, 64)
|
||||
err := collection.Find(query).All(&characters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return characters, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
collection = db.C("common.characters")
|
||||
|
||||
collection.EnsureIndexKey("name")
|
||||
collection.EnsureIndexKey("shortName")
|
||||
collection.EnsureIndexKey("author")
|
||||
err := collection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"nicks"},
|
||||
Unique: true,
|
||||
DropDups: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("init common.characters:", err)
|
||||
}
|
||||
err = collection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"$text:description"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("init common.characters:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/model/channel"
|
||||
"git.aiterp.net/rpdata/api/model/character"
|
||||
"git.aiterp.net/rpdata/api/model/counter"
|
||||
|
||||
"github.com/globalsign/mgo/bson"
|
||||
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
var postMutex sync.RWMutex
|
||||
|
||||
var characterUpdateMutex sync.Mutex
|
||||
|
||||
var logsCollection *mgo.Collection
|
||||
|
||||
// Log is the header/session for a log file.
|
||||
type Log struct {
|
||||
ID string `bson:"_id"`
|
||||
ShortID string `bson:"shortId"`
|
||||
Date time.Time `bson:"date"`
|
||||
Channel string `bson:"channel"`
|
||||
Title string `bson:"title,omitempty"`
|
||||
Event string `bson:"event,omitempty"`
|
||||
Description string `bson:"description,omitempty"`
|
||||
Open bool `bson:"open"`
|
||||
CharacterIDs []string `bson:"characterIds"`
|
||||
}
|
||||
|
||||
// New creates a new Log
|
||||
func New(date time.Time, channelName, title, event, description string, open bool) (Log, error) {
|
||||
nextID, err := counter.Next("auto_increment", "Log")
|
||||
if err != nil {
|
||||
return Log{}, err
|
||||
}
|
||||
|
||||
_, err = channel.Ensure(channelName, open)
|
||||
if err != nil {
|
||||
return Log{}, err
|
||||
}
|
||||
|
||||
log := Log{
|
||||
ID: MakeLogID(date, channelName),
|
||||
ShortID: "L" + strconv.Itoa(nextID),
|
||||
Date: date,
|
||||
Channel: channelName,
|
||||
Title: title,
|
||||
Event: event,
|
||||
Description: description,
|
||||
Open: open,
|
||||
CharacterIDs: nil,
|
||||
}
|
||||
|
||||
err = logsCollection.Insert(log)
|
||||
if err != nil {
|
||||
return Log{}, err
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// FindID finds a log either by it's ID or short ID.
|
||||
func FindID(id string) (Log, error) {
|
||||
return findLog(bson.M{
|
||||
"$or": []bson.M{
|
||||
bson.M{"_id": id},
|
||||
bson.M{"shortId": id},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// List lists all logs
|
||||
func List(limit int) ([]Log, error) {
|
||||
return listLog(bson.M{}, limit)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// If a text search is specified, it will make two trips to the database.
|
||||
func ListSearch(textSearch string, channels []string, characterIds []string, events []string, open bool, limit int) ([]Log, error) {
|
||||
postMutex.RLock()
|
||||
defer postMutex.RUnlock()
|
||||
|
||||
query := bson.M{}
|
||||
|
||||
// Run a text search
|
||||
if textSearch != "" {
|
||||
searchResults := make([]string, 0, 32)
|
||||
|
||||
err := postCollection.Find(bson.M{"$text": bson.M{"$search": textSearch}}).Distinct("logId", &searchResults)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Posts always use shortId to refer to the log
|
||||
query["shortId"] = bson.M{"$in": searchResults}
|
||||
}
|
||||
|
||||
// Find logs including any of the specified events and channels
|
||||
if len(channels) > 0 {
|
||||
query["channel"] = bson.M{"$in": channels}
|
||||
}
|
||||
if len(events) > 0 {
|
||||
query["events"] = bson.M{"$in": channels}
|
||||
}
|
||||
|
||||
// Find logs including all of the specified character IDs.
|
||||
if len(characterIds) > 0 {
|
||||
query["characterIds"] = bson.M{"$all": characterIds}
|
||||
}
|
||||
|
||||
// Limit to only open logs
|
||||
if open {
|
||||
query["open"] = true
|
||||
}
|
||||
|
||||
return listLog(query, limit)
|
||||
}
|
||||
|
||||
// Edit sets the metadata
|
||||
func (log *Log) Edit(title *string, event *string, description *string, open *bool) error {
|
||||
changes := bson.M{}
|
||||
|
||||
if title != nil && *title != log.Title {
|
||||
changes["title"] = *title
|
||||
}
|
||||
if event != nil && *event != log.Event {
|
||||
changes["event"] = *event
|
||||
}
|
||||
if description != nil && *description != log.Description {
|
||||
changes["description"] = *description
|
||||
}
|
||||
if open != nil && *open != log.Open {
|
||||
changes["open"] = *open
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := logsCollection.UpdateId(log.ID, bson.M{"$set": changes})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if title != nil {
|
||||
log.Title = *title
|
||||
}
|
||||
if event != nil {
|
||||
log.Event = *event
|
||||
}
|
||||
if description != nil {
|
||||
log.Description = *description
|
||||
}
|
||||
if open != nil {
|
||||
log.Open = *open
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Posts gets all the posts under the log. If no kinds are specified, it
|
||||
// will get all posts
|
||||
func (log *Log) Posts(kinds ...string) ([]Post, error) {
|
||||
postMutex.RLock()
|
||||
defer postMutex.RUnlock()
|
||||
|
||||
query := bson.M{
|
||||
"$or": []bson.M{
|
||||
bson.M{"logId": log.ID},
|
||||
bson.M{"logId": log.ShortID},
|
||||
},
|
||||
}
|
||||
|
||||
if len(kinds) > 0 {
|
||||
for i := range kinds {
|
||||
kinds[i] = strings.ToLower(kinds[i])
|
||||
}
|
||||
|
||||
query["kind"] = bson.M{"$in": kinds}
|
||||
}
|
||||
|
||||
posts, err := listPosts(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.SliceStable(posts, func(i, j int) bool {
|
||||
return posts[i].Position < posts[j].Position
|
||||
})
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
// NewPost creates a new post.
|
||||
func (log *Log) NewPost(time time.Time, kind, nick, text string) (Post, error) {
|
||||
if kind == "" || nick == "" || text == "" {
|
||||
return Post{}, errors.New("Missing/empty parameters")
|
||||
}
|
||||
|
||||
postMutex.RLock()
|
||||
defer postMutex.RUnlock()
|
||||
|
||||
position, err := counter.Next("next_post_id", log.ShortID)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
|
||||
post := Post{
|
||||
ID: MakePostID(time),
|
||||
Position: position,
|
||||
LogID: log.ShortID,
|
||||
Time: time,
|
||||
Kind: kind,
|
||||
Nick: nick,
|
||||
Text: text,
|
||||
}
|
||||
|
||||
err = postCollection.Insert(post)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
// UpdateCharacters updates the character list
|
||||
func (log *Log) UpdateCharacters() error {
|
||||
characterUpdateMutex.Lock()
|
||||
defer characterUpdateMutex.Unlock()
|
||||
|
||||
posts, err := log.Posts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
added := make(map[string]bool)
|
||||
removed := make(map[string]bool)
|
||||
for _, post := range posts {
|
||||
if post.Kind == "text" || post.Kind == "action" {
|
||||
if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Clean up the nick (remove possessive suffix, comma, formatting stuff)
|
||||
if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
|
||||
post.Nick = post.Nick[:len(post.Nick)-2]
|
||||
} else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
|
||||
post.Nick = post.Nick[:len(post.Nick)-1]
|
||||
}
|
||||
|
||||
added[post.Nick] = true
|
||||
}
|
||||
if post.Kind == "chars" {
|
||||
tokens := strings.Fields(post.Text)
|
||||
for _, token := range tokens {
|
||||
if strings.HasPrefix(token, "-") {
|
||||
removed[token[1:]] = true
|
||||
} else {
|
||||
added[strings.Replace(token, "+", "", 1)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nicks := make([]string, 0, len(added))
|
||||
for nick := range added {
|
||||
if added[nick] && !removed[nick] {
|
||||
nicks = append(nicks, nick)
|
||||
}
|
||||
}
|
||||
|
||||
characters, err := character.ListNicks(nicks...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
characterIDs := make([]string, len(characters))
|
||||
for i, char := range characters {
|
||||
characterIDs[i] = char.ID
|
||||
}
|
||||
|
||||
err = logsCollection.UpdateId(log.ID, bson.M{"$set": bson.M{"characterIds": characterIDs}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, nick := range nicks {
|
||||
found := false
|
||||
|
||||
for _, character := range characters {
|
||||
if character.HasNick(nick) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
addUnknownNick(nick)
|
||||
}
|
||||
}
|
||||
|
||||
log.CharacterIDs = characterIDs
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return Log{}, err
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func listLog(query interface{}, limit int) ([]Log, error) {
|
||||
logs := make([]Log, 0, 64)
|
||||
err := logsCollection.Find(query).Limit(limit).Sort("-date").All(&logs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func iterLogs(query interface{}, limit int) *mgo.Iter {
|
||||
return logsCollection.Find(query).Sort("-date").Limit(limit).Batch(8).Iter()
|
||||
}
|
||||
|
||||
// MakeLogID generates log IDs that are of the format from logbot2, though it will break compatibility.
|
||||
func MakeLogID(date time.Time, channel string) string {
|
||||
return fmt.Sprintf("%s%03d_%s", date.UTC().Format("2006-01-02_150405"), (date.Nanosecond() / int(time.Millisecond/time.Nanosecond)), channel[1:])
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
logsCollection = db.C("logbot3.logs")
|
||||
|
||||
logsCollection.EnsureIndexKey("date")
|
||||
logsCollection.EnsureIndexKey("channel")
|
||||
logsCollection.EnsureIndexKey("characterIds")
|
||||
logsCollection.EnsureIndexKey("event")
|
||||
logsCollection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"channel", "open"},
|
||||
})
|
||||
err := logsCollection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"shortId"},
|
||||
Unique: true,
|
||||
DropDups: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("init logbot3.logs:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var postCollection *mgo.Collection
|
||||
|
||||
// A Post is a part of a log file.
|
||||
type Post struct {
|
||||
ID string `bson:"_id"`
|
||||
LogID string `bson:"logId"`
|
||||
Time time.Time `bson:"time"`
|
||||
Kind string `bson:"kind"`
|
||||
Nick string `bson:"nick"`
|
||||
Text string `bson:"text"`
|
||||
Position int `bson:"position"`
|
||||
}
|
||||
|
||||
// Edit the post
|
||||
func (post *Post) Edit(time *time.Time, kind *string, nick *string, text *string) error {
|
||||
changes := bson.M{}
|
||||
changed := false
|
||||
postCopy := *post
|
||||
|
||||
if time != nil && !time.IsZero() && !time.Equal(post.Time) {
|
||||
changes["time"] = *time
|
||||
changed = true
|
||||
postCopy.Time = *time
|
||||
}
|
||||
if kind != nil && *kind != "" && *kind != post.Kind {
|
||||
changes["kind"] = *kind
|
||||
changed = true
|
||||
postCopy.Kind = *kind
|
||||
}
|
||||
if nick != nil && *nick != "" && *nick != post.Nick {
|
||||
changes["nick"] = *nick
|
||||
changed = true
|
||||
postCopy.Nick = *nick
|
||||
}
|
||||
if text != nil && *text != "" && *text != post.Text {
|
||||
changes["text"] = *text
|
||||
changed = true
|
||||
postCopy.Text = *text
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := postCollection.UpdateId(post.ID, bson.M{"$set": changes})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*post = postCopy
|
||||
return nil
|
||||
}
|
||||
|
||||
// Move the post
|
||||
func (post *Post) Move(toPosition int) error {
|
||||
if toPosition < 1 {
|
||||
return errors.New("Invalid position")
|
||||
}
|
||||
|
||||
postMutex.Lock()
|
||||
defer postMutex.Unlock()
|
||||
|
||||
// To avoid problems, only allow target indices that are allowed. If it's 1, then there is bound to
|
||||
// be a post at the position.
|
||||
if toPosition > 1 {
|
||||
existingPost := Post{}
|
||||
err := postCollection.Find(bson.M{"logId": post.LogID, "position": toPosition}).One(&existingPost)
|
||||
|
||||
if err != nil || existingPost.Position != toPosition {
|
||||
return errors.New("No post found at the position")
|
||||
}
|
||||
}
|
||||
|
||||
query := bson.M{"logId": post.LogID}
|
||||
operation := bson.M{"$inc": bson.M{"position": 1}}
|
||||
|
||||
if toPosition < post.Position {
|
||||
query["$and"] = []bson.M{
|
||||
bson.M{"position": bson.M{"$gte": toPosition}},
|
||||
bson.M{"position": bson.M{"$lt": post.Position}},
|
||||
}
|
||||
} else {
|
||||
query["$and"] = []bson.M{
|
||||
bson.M{"position": bson.M{"$gt": post.Position}},
|
||||
bson.M{"position": bson.M{"$lte": toPosition}},
|
||||
}
|
||||
|
||||
operation["$inc"] = bson.M{"position": -1}
|
||||
}
|
||||
|
||||
_, err := postCollection.UpdateAll(query, operation)
|
||||
if err != nil {
|
||||
return errors.New("moving others: " + err.Error())
|
||||
}
|
||||
|
||||
err = postCollection.UpdateId(post.ID, bson.M{"$set": bson.M{"position": toPosition}})
|
||||
if err != nil {
|
||||
return errors.New("moving: " + err.Error())
|
||||
}
|
||||
|
||||
post.Position = toPosition
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindPostID finds a log post by ID.
|
||||
func FindPostID(id string) (Post, error) {
|
||||
return findPost(bson.M{"_id": id})
|
||||
}
|
||||
|
||||
// ListPostIDs lists log posts by ID
|
||||
func ListPostIDs(ids ...string) ([]Post, error) {
|
||||
return listPosts(bson.M{"_id": bson.M{"$in": ids}})
|
||||
}
|
||||
|
||||
// RemovePost removes a post, moving all subsequent post up one position
|
||||
func RemovePost(id string) (Post, error) {
|
||||
postMutex.Lock()
|
||||
defer postMutex.Unlock()
|
||||
|
||||
post, err := findPost(bson.M{"_id": id})
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
|
||||
err = postCollection.RemoveId(id)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
|
||||
_, err = postCollection.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func findPost(query interface{}) (Post, error) {
|
||||
post := Post{}
|
||||
err := postCollection.Find(query).One(&post)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func listPosts(query interface{}) ([]Post, error) {
|
||||
posts := make([]Post, 0, 64)
|
||||
err := postCollection.Find(query).All(&posts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
// MakePostID makes a random post ID
|
||||
func MakePostID(time time.Time) string {
|
||||
data := make([]byte, 4)
|
||||
rand.Read(data)
|
||||
|
||||
return "P" + strconv.FormatInt(time.UnixNano(), 36) + strconv.FormatInt(int64(binary.LittleEndian.Uint32(data)), 36)
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
postCollection = db.C("logbot3.posts")
|
||||
|
||||
postCollection.EnsureIndexKey("logId")
|
||||
postCollection.EnsureIndexKey("time")
|
||||
postCollection.EnsureIndexKey("kind")
|
||||
postCollection.EnsureIndexKey("position")
|
||||
|
||||
err := postCollection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"$text:text"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("init logbot3.logs:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var unknownConnection *mgo.Collection
|
||||
|
||||
// An UnknownNick is a nick found by the character list updater that
|
||||
// does not exist. The score is the number of logs that nick was in, meaning
|
||||
// nicks with a higher score should be a high priority to be matched with
|
||||
// a character.
|
||||
type UnknownNick struct {
|
||||
Nick string `bson:"_id" json:"nick"`
|
||||
Score int `bson:"score" json:"score"`
|
||||
}
|
||||
|
||||
// UnknownNicks gets all the unknown nicks from the last search.
|
||||
func UnknownNicks() ([]UnknownNick, error) {
|
||||
nicks := make([]UnknownNick, 0, 256)
|
||||
err := unknownConnection.Find(bson.M{}).Sort("-score").All(&nicks)
|
||||
|
||||
return nicks, err
|
||||
}
|
||||
|
||||
func addUnknownNick(nick string) error {
|
||||
_, err := unknownConnection.UpsertId(nick, bson.M{"$inc": bson.M{"score": 1}})
|
||||
return err
|
||||
}
|
||||
|
||||
func clearUnknownNicks() error {
|
||||
_, err := unknownConnection.RemoveAll(bson.M{})
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
unknownConnection = db.C("logbot3.unknown_nicks")
|
||||
})
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var scheduleCharacterUpdate = func() func() {
|
||||
var mutex sync.Mutex
|
||||
var scheduled bool
|
||||
|
||||
return func() {
|
||||
mutex.Lock()
|
||||
if !scheduled {
|
||||
go func() {
|
||||
time.Sleep(time.Second * 60)
|
||||
|
||||
// If another comes along in the next 2-3 seconds, it should schedule a new
|
||||
// round to avoid a character only appearing in half their logs.
|
||||
mutex.Lock()
|
||||
scheduled = false
|
||||
mutex.Unlock()
|
||||
|
||||
UpdateAllCharacters()
|
||||
}()
|
||||
|
||||
scheduled = true
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// ScheduleCharacterUpdate schedules a full update within the minute.
|
||||
// Subsequent calls within that time will not schedule anything. Even
|
||||
// if the operation takes a few seconds at most, it need not be ran often.
|
||||
func ScheduleCharacterUpdate() {
|
||||
scheduleCharacterUpdate()
|
||||
}
|
||||
|
||||
// UpdateCharacters is a shorthand for getting a log and updaing its characters
|
||||
func UpdateCharacters(logID string) error {
|
||||
log, err := FindID(logID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return log.UpdateCharacters()
|
||||
}
|
||||
|
||||
// UpdateAllCharacters updates character list on all logs. This should
|
||||
// be done if one or more characters failed to be added.
|
||||
func UpdateAllCharacters() (updated int, err error) {
|
||||
updated = 0
|
||||
|
||||
err = clearUnknownNicks()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
iter := iterLogs(bson.M{}, 0)
|
||||
err = iter.Err()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log := Log{}
|
||||
for iter.Next(&log) {
|
||||
err = log.UpdateCharacters()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
updated++
|
||||
}
|
||||
|
||||
err = iter.Err()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package story
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var chapterCollection *mgo.Collection
|
||||
|
||||
// A Chapter is a part of a story.
|
||||
type Chapter struct {
|
||||
ID string `bson:"_id"`
|
||||
StoryID string `bson:"storyId"`
|
||||
Title string `bson:"title"`
|
||||
Author string `bson:"author"`
|
||||
Source string `bson:"source"`
|
||||
CreatedDate time.Time `bson:"createdDate"`
|
||||
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
|
||||
EditedDate time.Time `bson:"editedDate"`
|
||||
}
|
||||
|
||||
// Edit edits a chapter, and updates EditedDate. While many Edit functions cheat if there's nothing to
|
||||
// change, this functill will due to EditedDate.
|
||||
func (chapter *Chapter) Edit(title, source *string, fictionalDate *time.Time) error {
|
||||
now := time.Now()
|
||||
changes := bson.M{"editedDate": now}
|
||||
changed := *chapter
|
||||
changed.EditedDate = now
|
||||
|
||||
if title != nil && *title != chapter.Title {
|
||||
changes["title"] = *title
|
||||
changed.Title = *title
|
||||
}
|
||||
if source != nil && *source != chapter.Source {
|
||||
changes["source"] = *source
|
||||
changed.Source = *source
|
||||
}
|
||||
if fictionalDate != nil && !fictionalDate.Equal(chapter.FictionalDate) {
|
||||
changes["fictionalDate"] = *fictionalDate
|
||||
changed.FictionalDate = *fictionalDate
|
||||
}
|
||||
|
||||
err := chapterCollection.UpdateId(chapter.ID, bson.M{"$set": changes})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*chapter = changed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a chapter.
|
||||
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"
|
||||
offset := 0
|
||||
data := make([]byte, 32)
|
||||
|
||||
rand.Read(data)
|
||||
for len(result) < 24 {
|
||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
||||
offset += 8
|
||||
|
||||
if offset >= 32 {
|
||||
rand.Read(data)
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
return result[:24]
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
chapterCollection = db.C("story.chapters")
|
||||
|
||||
chapterCollection.EnsureIndexKey("storyId")
|
||||
chapterCollection.EnsureIndexKey("author")
|
||||
chapterCollection.EnsureIndexKey("createdDate")
|
||||
})
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
package story
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var storyCollection *mgo.Collection
|
||||
|
||||
// ErrTagAlreadyExists is an error returned by Story.AddTag
|
||||
var ErrTagAlreadyExists = errors.New("Tag already exists")
|
||||
|
||||
// ErrTagNotExists is an error returned by Story.RemoveTag
|
||||
var ErrTagNotExists = errors.New("Tag does not exist")
|
||||
|
||||
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
|
||||
// The story model is a container for multiple chapters this time, in contrast to the previous version.
|
||||
type Story struct {
|
||||
ID string `bson:"_id"`
|
||||
Author string `bson:"author"`
|
||||
Name string `bson:"name"`
|
||||
Category string `bson:"category"`
|
||||
Open bool `bson:"open"`
|
||||
Listed bool `bson:"listed"`
|
||||
Tags []Tag `bson:"tags"`
|
||||
CreatedDate time.Time `bson:"createdDate"`
|
||||
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
|
||||
UpdatedDate time.Time `bson:"updatedDate"`
|
||||
}
|
||||
|
||||
// AddTag adds a tag to the story. It returns ErrTagAlreadyExists if the tag is already there
|
||||
func (story *Story) AddTag(tag Tag) error {
|
||||
for i := range story.Tags {
|
||||
if story.Tags[i].Equal(tag) {
|
||||
return ErrTagAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
err := storyCollection.UpdateId(story.ID, bson.M{"$push": bson.M{"tags": tag}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
story.Tags = append(story.Tags, tag)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTag removes a tag to the story. It returns ErrTagNotExists if the tag does not exist.
|
||||
func (story *Story) RemoveTag(tag Tag) error {
|
||||
index := -1
|
||||
for i := range story.Tags {
|
||||
if story.Tags[i].Equal(tag) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return ErrTagNotExists
|
||||
}
|
||||
|
||||
err := storyCollection.UpdateId(story.ID, bson.M{"$pull": bson.M{"tags": tag}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
story.Tags = append(story.Tags[:index], story.Tags[index+1:]...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edit edits the story, reflecting the new values in the story's struct values. If nothing will be
|
||||
// changed, it will silently return without a database roundtrip.
|
||||
func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDate *time.Time) error {
|
||||
changes := bson.M{}
|
||||
changed := *story
|
||||
|
||||
if name != nil && *name != story.Name {
|
||||
changes["name"] = *name
|
||||
changed.Name = *name
|
||||
}
|
||||
if category != nil && *category != story.Category {
|
||||
changes["category"] = *category
|
||||
changed.Name = *category
|
||||
}
|
||||
if listed != nil && *listed != story.Listed {
|
||||
changes["listed"] = *listed
|
||||
changed.Listed = *listed
|
||||
}
|
||||
if open != nil && *open != story.Open {
|
||||
changes["open"] = *open
|
||||
changed.Open = *open
|
||||
}
|
||||
if fictionalDate != nil && !fictionalDate.Equal(story.FictionalDate) {
|
||||
changes["fictionalDate"] = *fictionalDate
|
||||
changed.FictionalDate = *fictionalDate
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := storyCollection.UpdateId(story.ID, bson.M{"$set": changes})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*story = changed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the story from the database
|
||||
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) {
|
||||
if !story.Open && author != story.Author {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: AddChapter is breaking Open rules (story.id=%#+v, story.name=%#+v, chapter.author=%#+v, chapter.title=%#+v)", story.ID, story.Name, author, title)
|
||||
}
|
||||
|
||||
chapter := Chapter{
|
||||
ID: makeChapterID(),
|
||||
StoryID: story.ID,
|
||||
Title: title,
|
||||
Author: author,
|
||||
Source: source,
|
||||
CreatedDate: createdDate,
|
||||
FictionalDate: finctionalDate,
|
||||
EditedDate: createdDate,
|
||||
}
|
||||
|
||||
err := chapterCollection.Insert(chapter)
|
||||
if err != nil {
|
||||
return Chapter{}, err
|
||||
}
|
||||
|
||||
if createdDate.After(story.UpdatedDate) {
|
||||
if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil {
|
||||
story.UpdatedDate = createdDate
|
||||
}
|
||||
}
|
||||
|
||||
return chapter, nil
|
||||
}
|
||||
|
||||
// New creates a new story.
|
||||
func New(name, author, category string, listed, open bool, tags []Tag, createdDate, fictionalDate time.Time) (Story, error) {
|
||||
story := Story{
|
||||
ID: makeStoryID(),
|
||||
Name: name,
|
||||
Author: author,
|
||||
Category: category,
|
||||
Listed: listed,
|
||||
Open: open,
|
||||
Tags: tags,
|
||||
CreatedDate: createdDate,
|
||||
FictionalDate: fictionalDate,
|
||||
UpdatedDate: createdDate,
|
||||
}
|
||||
|
||||
err := storyCollection.Insert(story)
|
||||
if err != nil {
|
||||
return Story{}, err
|
||||
}
|
||||
|
||||
return story, nil
|
||||
}
|
||||
|
||||
// FindID finds a story by ID
|
||||
func FindID(id string) (Story, error) {
|
||||
story := Story{}
|
||||
err := storyCollection.FindId(id).One(&story)
|
||||
|
||||
return story, err
|
||||
}
|
||||
|
||||
// List lists stories by any non-zero criteria passed with it.
|
||||
func List(author string, tags []Tag, earliest, latest time.Time, limit int) ([]Story, error) {
|
||||
query := bson.M{}
|
||||
|
||||
if author != "" {
|
||||
query["author"] = author
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
query["tags"] = bson.M{"$in": tags}
|
||||
}
|
||||
|
||||
if !earliest.IsZero() && !latest.IsZero() {
|
||||
query["fictionalDate"] = bson.M{
|
||||
"$gte": earliest,
|
||||
"$lt": latest,
|
||||
}
|
||||
} else if !latest.IsZero() {
|
||||
query["fictionalDate"] = bson.M{
|
||||
"$lt": latest,
|
||||
}
|
||||
} else if !earliest.IsZero() {
|
||||
query["fictionalDate"] = bson.M{
|
||||
"$gte": earliest,
|
||||
}
|
||||
}
|
||||
|
||||
stories := make([]Story, 0, 128)
|
||||
err := storyCollection.Find(query).Limit(limit).All(&stories)
|
||||
|
||||
return stories, err
|
||||
}
|
||||
|
||||
// makeStoryID makes a random story ID that's 16 characters long
|
||||
func makeStoryID() string {
|
||||
result := "S"
|
||||
offset := 0
|
||||
data := make([]byte, 32)
|
||||
|
||||
rand.Read(data)
|
||||
for len(result) < 16 {
|
||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
||||
offset += 8
|
||||
|
||||
if offset >= 32 {
|
||||
rand.Read(data)
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
return result[:16]
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
storyCollection = db.C("story.stories")
|
||||
|
||||
storyCollection.EnsureIndexKey("tags")
|
||||
storyCollection.EnsureIndexKey("author")
|
||||
storyCollection.EnsureIndexKey("updatedDate")
|
||||
storyCollection.EnsureIndexKey("fictionalDate")
|
||||
storyCollection.EnsureIndexKey("listed")
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user