Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ad0b9d467 | ||
|
|
63bc262091 | ||
|
|
c4eabd69ac | ||
|
|
df982fac17 | ||
|
|
6fa2dc9800 | ||
|
|
6ce38d61b1 | ||
|
|
dc3f77f8da | ||
|
|
6321ea1893 | ||
|
|
9b346fb97b | ||
|
|
ed21b7c8cd | ||
|
|
47b7877ab4 | ||
|
|
757b0bc616 | ||
|
|
5dbed6d632 | ||
|
|
c8ced52e1a | ||
|
|
8a313b3bb2 | ||
|
|
e690e7ab97 | ||
|
|
0e70e37038 | ||
|
|
3b10c4d9b1 | ||
|
|
690eb4f121 | ||
|
|
2eed815993 | ||
|
|
8c4670580c | ||
|
|
8fbaea2078 | ||
|
|
2a0e6534ff | ||
|
|
4bd75b8240 | ||
|
|
ed38d576b2 | ||
|
|
51b2c9bfe0 | ||
|
|
c0687d76ad | ||
|
|
60dcd8a80f | ||
|
|
5bcc7479ff | ||
|
|
2c00358f4f | ||
|
|
acb6b637a5 | ||
|
|
f46b6f0774 | ||
|
|
73a1c79903 | ||
|
|
e194b6536e | ||
|
|
f6ad0602c2 | ||
|
|
021f67ea4b | ||
|
|
13f82399ef | ||
|
|
2a1423d28f | ||
|
|
7b253748fe | ||
|
|
53c3d4597b | ||
|
|
cdf4e6f667 | ||
|
|
6fa9f64f2d | ||
|
|
2fb391321e | ||
|
|
015bdfd2e8 | ||
|
|
9925907e93 | ||
|
|
c7c418aeab | ||
|
|
d5573e7f39 | ||
|
|
e794311eb8 | ||
|
|
aff0d99842 | ||
|
|
00150118d2 | ||
|
|
332a1eb32b | ||
|
|
db4d46c368 | ||
|
|
a8c8610ca2 | ||
|
|
d09c17d30d | ||
|
|
e3e3ea07db | ||
|
|
22592439da | ||
|
|
dfa9fb4025 | ||
|
|
0e2a68a2d4 | ||
|
|
1eefa65527 | ||
|
|
f9b6af58c9 | ||
|
|
56ecf48b57 | ||
|
|
06fbfc90c5 | ||
|
|
39a4fef506 | ||
|
|
f9fe0bb5f8 | ||
|
|
d22ab387dd | ||
|
|
53a86c0b9c | ||
|
|
e0b93c6b4b | ||
|
|
1ea04f1577 | ||
|
|
17fd3c60f8 | ||
|
|
c5b62a42db | ||
|
|
fa0926c311 | ||
|
|
d21efc1c7f | ||
|
|
852030b141 | ||
|
|
f7f0b76379 | ||
|
|
2711654816 | ||
|
|
b60897a597 | ||
|
|
80e6c048c9 | ||
|
|
6310cbbc1e | ||
|
|
89a1c2366c | ||
|
|
ac2b79e88a | ||
|
|
31239330df | ||
|
|
a58535fcb9 | ||
|
|
2a91b6f9bc | ||
|
|
caa7727c02 | ||
|
|
047c9784ae |
+33
-15
@@ -1,21 +1,39 @@
|
|||||||
workspace:
|
name: rpdata-api
|
||||||
base: /go
|
|
||||||
path: src/git.aiterp.net/rpdata/api
|
|
||||||
|
|
||||||
pipeline:
|
kind: pipeline
|
||||||
docker-release:
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: go-test
|
||||||
|
image: golang:1.14
|
||||||
|
commands:
|
||||||
|
- go generate ./...
|
||||||
|
- go test -v ./...
|
||||||
|
|
||||||
|
- name: docker-release-tag
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
repo: vmaple/rpdata
|
settings:
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
secrets: [ docker_username, docker_password ]
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: r.vmaple.dev/rpdata/rpdata-api
|
||||||
|
registry: r.vmaple.dev
|
||||||
when:
|
when:
|
||||||
event: tag
|
event:
|
||||||
ref: refs/tags/*
|
- tag
|
||||||
docker-release-latest:
|
|
||||||
|
- name: docker-release-latest
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
repo: vmaple/rpdata
|
settings:
|
||||||
tags: latest
|
auto_tag: false
|
||||||
secrets: [ docker_username, docker_password ]
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: r.vmaple.dev/rpdata/rpdata-api
|
||||||
|
registry: r.vmaple.dev
|
||||||
when:
|
when:
|
||||||
event: tag
|
event:
|
||||||
ref: refs/tags/*
|
- tag
|
||||||
@@ -8,6 +8,11 @@ rpdata-graphiql
|
|||||||
rpdata-*
|
rpdata-*
|
||||||
!rpdata-*/
|
!rpdata-*/
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
|
/main
|
||||||
|
/main.exe
|
||||||
|
|
||||||
|
/graph2/graphcore/*_gen.go
|
||||||
generated.gql
|
generated.gql
|
||||||
generated.go
|
generated.go
|
||||||
+7
-18
@@ -1,34 +1,23 @@
|
|||||||
## 1. Build Server
|
## 1. Build Server
|
||||||
# Use Go 1.10
|
# Use Go 1.14
|
||||||
FROM golang:1.11 AS build-server
|
FROM golang:1.14 AS build-server
|
||||||
|
|
||||||
# Load repository into docker
|
# Load repository into docker
|
||||||
WORKDIR /go/src/git.aiterp.net/rpdata/api/
|
WORKDIR /project/
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN rm -rf ./vendor
|
|
||||||
|
|
||||||
# Load build tools and dependencies
|
# Install 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 ./...
|
RUN go generate ./...
|
||||||
|
|
||||||
# Build server and tools (CGO disabled to make them 100% static)
|
# Build server and tools (CGO disabled to make them 100% static)
|
||||||
ENV CGO_ENABLED 0
|
ENV CGO_ENABLED 0
|
||||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-server ./cmd/rpdata-server
|
RUN go build -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
|
|
||||||
# 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
|
|
||||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-logpaste ./cmd/rpdata-logpaste
|
|
||||||
|
|
||||||
## 2. Distribute
|
## 2. Distribute
|
||||||
# Use alpine linux
|
# Use alpine linux
|
||||||
FROM alpine:3.7
|
FROM alpine:3.12
|
||||||
|
|
||||||
# Install certificates to enable HTTPS GET requests, and tzdata for logpaste
|
# Install certificates to enable HTTPS GET requests, and tzdata for log imports.
|
||||||
RUN apk add --no-cache ca-certificates tzdata
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
# Copy build files into final container
|
# Copy build files into final container
|
||||||
|
|||||||
Generated
-254
@@ -1,254 +0,0 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
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 = "3d21ba515fe27b856f230847e856431ae1724adc"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/dgrijalva/jwt-go"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
|
||||||
version = "v3.2.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/dustin/go-humanize"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/globalsign/mgo"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"bson",
|
|
||||||
"internal/json",
|
|
||||||
"internal/sasl",
|
|
||||||
"internal/scram"
|
|
||||||
]
|
|
||||||
revision = "f76e4f9da92ecd56e3be26f5ba92580af1ef97b4"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/go-ini/ini"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "ace140f73450505f33e8b8418216792275ae82a7"
|
|
||||||
version = "v1.35.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/go-sql-driver/mysql"
|
|
||||||
packages = ["."]
|
|
||||||
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 = ["."]
|
|
||||||
revision = "78139374585c29dcb97b8f33089ed11959e4be59"
|
|
||||||
version = "v5"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/graph-gophers/graphql-go"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"errors",
|
|
||||||
"internal/common",
|
|
||||||
"internal/exec",
|
|
||||||
"internal/exec/packer",
|
|
||||||
"internal/exec/resolvable",
|
|
||||||
"internal/exec/selected",
|
|
||||||
"internal/query",
|
|
||||||
"internal/schema",
|
|
||||||
"internal/validation",
|
|
||||||
"introspection",
|
|
||||||
"log",
|
|
||||||
"relay",
|
|
||||||
"trace"
|
|
||||||
]
|
|
||||||
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"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"reflectx"
|
|
||||||
]
|
|
||||||
revision = "0dae4fefe7c0e190f7b5a78dac28a1c82cc8d849"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/minio/minio-go"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"pkg/credentials",
|
|
||||||
"pkg/encrypt",
|
|
||||||
"pkg/s3signer",
|
|
||||||
"pkg/s3utils",
|
|
||||||
"pkg/set"
|
|
||||||
]
|
|
||||||
revision = "3d2d02921f0510e9d1f66ef77a265b8dddd36992"
|
|
||||||
version = "6.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/go-homedir"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/opentracing/opentracing-go"
|
|
||||||
packages = [
|
|
||||||
".",
|
|
||||||
"ext",
|
|
||||||
"log"
|
|
||||||
]
|
|
||||||
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
|
|
||||||
version = "v1.0.2"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
|
||||||
version = "v0.8.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/sirupsen/logrus"
|
|
||||||
packages = ["."]
|
|
||||||
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"
|
|
||||||
packages = [
|
|
||||||
"argon2",
|
|
||||||
"blake2b",
|
|
||||||
"ssh/terminal"
|
|
||||||
]
|
|
||||||
revision = "d6449816ce06963d9d136eee5a56fca5b0616e7e"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/net"
|
|
||||||
packages = [
|
|
||||||
"context",
|
|
||||||
"idna",
|
|
||||||
"lex/httplex"
|
|
||||||
]
|
|
||||||
revision = "a35a21de978d84ffc92f010a153705b170b2f9d1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
packages = [
|
|
||||||
"unix",
|
|
||||||
"windows"
|
|
||||||
]
|
|
||||||
revision = "2f57af4873d00d535c5c9028850aa2152e6a5566"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "golang.org/x/text"
|
|
||||||
packages = [
|
|
||||||
"collate",
|
|
||||||
"collate/build",
|
|
||||||
"internal/colltab",
|
|
||||||
"internal/gen",
|
|
||||||
"internal/tag",
|
|
||||||
"internal/triegen",
|
|
||||||
"internal/ucd",
|
|
||||||
"language",
|
|
||||||
"secure/bidirule",
|
|
||||||
"transform",
|
|
||||||
"unicode/bidi",
|
|
||||||
"unicode/cldr",
|
|
||||||
"unicode/norm",
|
|
||||||
"unicode/rangetable"
|
|
||||||
]
|
|
||||||
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 = "3c9430afa8b9260026925c8f496af934e2e573a2bd4587bb634fd12a20ce6c2c"
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
# Gopkg.toml example
|
|
||||||
#
|
|
||||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
|
||||||
# for detailed Gopkg.toml documentation.
|
|
||||||
#
|
|
||||||
# required = ["github.com/user/thing/cmd/thing"]
|
|
||||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project"
|
|
||||||
# version = "1.0.0"
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project2"
|
|
||||||
# branch = "dev"
|
|
||||||
# source = "github.com/myfork/project2"
|
|
||||||
#
|
|
||||||
# [[override]]
|
|
||||||
# name = "github.com/x/y"
|
|
||||||
# version = "2.4.0"
|
|
||||||
#
|
|
||||||
# [prune]
|
|
||||||
# non-go = false
|
|
||||||
# go-tests = true
|
|
||||||
# unused-packages = true
|
|
||||||
|
|
||||||
required = [ "github.com/99designs/gqlgen" ]
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/globalsign/mgo"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/minio/minio-go"
|
|
||||||
version = "6.0.0"
|
|
||||||
|
|
||||||
[prune]
|
|
||||||
go-tests = true
|
|
||||||
unused-packages = true
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/graph-gophers/graphql-go"
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
var flagHost = flag.String("host", "127.0.0.1:3306", "SQL host")
|
|
||||||
var flagDB = flag.String("database", "aitestory", "SQL database")
|
|
||||||
var flagUser = flag.String("user", "aitestory", "SQL user")
|
|
||||||
var flagPassword = flag.String("password", "", "SQL password")
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
db, err := sqlx.Connect("mysql", fmt.Sprintf("%s:%s@(%s)/%s", *flagUser, *flagPassword, *flagHost, *flagDB))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.Ping()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
store.Init()
|
|
||||||
|
|
||||||
results := make([]storyResult, 0, 64)
|
|
||||||
rows, err := db.Queryx("SELECT * FROM page WHERE unlisted=0;")
|
|
||||||
for rows.Next() {
|
|
||||||
result := storyResult{}
|
|
||||||
err := rows.StructScan(&result)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
results = append(results, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
tagResults := make([]tagResult, 0, 256)
|
|
||||||
rows, err = db.Queryx("SELECT page_id,type,name FROM page_tag LEFT JOIN tag ON tag_id=tag.id;")
|
|
||||||
for rows.Next() {
|
|
||||||
result := tagResult{}
|
|
||||||
err := rows.StructScan(&result)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
tagResults = append(tagResults, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, result := range results {
|
|
||||||
fictionalDate, err := time.Parse("2006-01-02 15:04:05", result.FictionalDate)
|
|
||||||
if err != nil {
|
|
||||||
if result.FictionalDate != "0000-00-00 00:00:00" {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fictionalDate.Year() < 1800 {
|
|
||||||
fictionalDate = time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
publishDate, err := time.Parse("2006-01-02 15:04:05", result.PublishDate)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := make([]models.Tag, 0, 8)
|
|
||||||
for _, tagResult := range tagResults {
|
|
||||||
if tagResult.PageID == result.ID {
|
|
||||||
tags = append(tags, models.Tag{Kind: models.TagKind(tagResult.Type), Name: tagResult.Name})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change the story title
|
|
||||||
title := result.Name
|
|
||||||
if strings.HasPrefix(result.Source, "#") {
|
|
||||||
firstNewline := strings.Index(result.Source, "\n")
|
|
||||||
result.Name = strings.Replace(result.Source[1:firstNewline], "\r", "", -1)
|
|
||||||
result.Source = result.Source[firstNewline+1:]
|
|
||||||
|
|
||||||
result.Source = strings.Replace(result.Source, "\r\n", "\n", -1)
|
|
||||||
if strings.HasPrefix(result.Source, "\r") || strings.HasPrefix(result.Source, "\n") {
|
|
||||||
result.Source = result.Source[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter, err := chapters.Add(story, title, result.Author[5:], result.Source, publishDate, &fictionalDate)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(result.ID, "->", story.ID, chapter.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type tagResult struct {
|
|
||||||
PageID string `db:"page_id"`
|
|
||||||
Type string `db:"type"`
|
|
||||||
Name string `db:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type storyResult struct {
|
|
||||||
ID string `db:"id"`
|
|
||||||
Name string `db:"name"`
|
|
||||||
Author string `db:"author"`
|
|
||||||
Category string `db:"category"`
|
|
||||||
FictionalDate string `db:"fictional_date"`
|
|
||||||
PublishDate string `db:"publish_date"`
|
|
||||||
EditDate string `db:"edit_date"`
|
|
||||||
Unlisted bool `db:"unlisted"`
|
|
||||||
Dated bool `db:"dated"`
|
|
||||||
Spesific bool `db:"specific"`
|
|
||||||
Indexed bool `db:"indexed"`
|
|
||||||
Published bool `db:"published"`
|
|
||||||
Type string `db:"type"`
|
|
||||||
Source string `db:"source"`
|
|
||||||
Cache string `db:"cache"`
|
|
||||||
BackgroundURL *string `db:"background_url"`
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/models/channels"
|
|
||||||
"git.aiterp.net/rpdata/api/models/logs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := store.Init()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, err := logs.List(nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
added := make(map[string]bool, 1024)
|
|
||||||
for _, log := range logs {
|
|
||||||
if added[log.ChannelName] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := channels.Ensure(log.ChannelName, false)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, log.ID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
added[log.ChannelName] = true
|
|
||||||
|
|
||||||
fmt.Println(log.ChannelName, "ensured")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
characters.json
|
|
||||||
characters.cson
|
|
||||||
debug
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type charInfo struct {
|
|
||||||
Nicks []string `json:"nicks"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Author string `json:"player"`
|
|
||||||
ShortName string `json:"first"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func load(reader io.Reader) ([]charInfo, error) {
|
|
||||||
data := make(map[string]interface{})
|
|
||||||
err := json.NewDecoder(reader).Decode(&data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
links := make(map[string]string, len(data))
|
|
||||||
infos := make([]charInfo, 0, 64)
|
|
||||||
|
|
||||||
for key, value := range data {
|
|
||||||
if info, ok := value.(map[string]interface{}); ok {
|
|
||||||
name := info["name"].(string)
|
|
||||||
author := info["player"].(string)
|
|
||||||
shortName, ok := info["first"].(string)
|
|
||||||
if !ok {
|
|
||||||
shortName = strings.SplitN(name, " ", 2)[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
infos = append(infos, charInfo{
|
|
||||||
Nicks: []string{key},
|
|
||||||
Name: name,
|
|
||||||
Author: author,
|
|
||||||
ShortName: shortName,
|
|
||||||
})
|
|
||||||
} else if nick, ok := value.(string); ok {
|
|
||||||
links[key] = nick
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range links {
|
|
||||||
for i := range infos {
|
|
||||||
if infos[i].Nicks[0] == value {
|
|
||||||
infos[i].Nicks = append(infos[i].Nicks, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return infos, nil
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/models/characters"
|
|
||||||
)
|
|
||||||
|
|
||||||
var fileName = flag.String("file", "./characters.json", "json file to load")
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
file, err := os.Open(*fileName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Open:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
infos, err := load(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Parse:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Store:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
charsAdded, charsFailed := 0, 0
|
|
||||||
nicksAdded, nicksFailed := 0, 0
|
|
||||||
|
|
||||||
for _, info := range infos {
|
|
||||||
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 = characters.FindNick(info.Nicks[0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Println(info.Nicks[0], "added")
|
|
||||||
charsAdded++
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, alt := range info.Nicks[1:] {
|
|
||||||
char, err = characters.AddNick(char, alt)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(info.Nicks[0], "failed to add nick", alt, "error:", err)
|
|
||||||
nicksFailed++
|
|
||||||
} else {
|
|
||||||
log.Println(info.Nicks[0], "addded nick", alt)
|
|
||||||
nicksAdded++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Characters – %d/%d", charsAdded, charsFailed+charsAdded)
|
|
||||||
log.Printf("Alt. Nicks – %d/%d", nicksAdded, nicksFailed+nicksAdded)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
logs
|
|
||||||
debug
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type logLine struct {
|
|
||||||
Verb string
|
|
||||||
Args []string
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFile(reader io.Reader) []logLine {
|
|
||||||
bufReader := bufio.NewReader(reader)
|
|
||||||
results := make([]logLine, 0, 512)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := bufReader.ReadString('\n')
|
|
||||||
if err == io.EOF {
|
|
||||||
if len(line) < 2 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(line) <= 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
line = strings.Replace(line, "\n", "", 1)
|
|
||||||
line = strings.Replace(line, "\r", "", 1)
|
|
||||||
|
|
||||||
results = append(results, parseLine(line))
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLine(line string) logLine {
|
|
||||||
textSplit := strings.SplitN(line, " :", 2)
|
|
||||||
tokens := strings.Split(textSplit[0], " ")
|
|
||||||
|
|
||||||
ll := logLine{
|
|
||||||
Verb: tokens[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tokens) > 1 {
|
|
||||||
ll.Args = tokens[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(textSplit) > 1 {
|
|
||||||
ll.Text = textSplit[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ll
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFilename(fname string) (date time.Time, channel string, err error) {
|
|
||||||
date, err = time.ParseInLocation("2006-01-02_150405", fname[:17], time.Local)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ms, err := strconv.Atoi(fname[17:20])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
date = date.Add(time.Duration(ms) * time.Millisecond)
|
|
||||||
|
|
||||||
if len(fname) < 23 {
|
|
||||||
err = errors.New("filename too short")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
channel = fname[21:]
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/posts"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/models/logs"
|
|
||||||
)
|
|
||||||
|
|
||||||
var prefixReplacer = strings.NewReplacer("+", "", "@", "", "!", "", "%", "")
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := store.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, filepath := range os.Args[1:] {
|
|
||||||
name := strings.Replace(path.Base(filepath), ".txt", "", 1)
|
|
||||||
file, err := os.Open(filepath)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(filepath, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logLines := parseFile(file)
|
|
||||||
file.Close()
|
|
||||||
|
|
||||||
// Get title and event
|
|
||||||
title := ""
|
|
||||||
event := ""
|
|
||||||
for _, line := range logLines {
|
|
||||||
if line.Verb == "TITLE" {
|
|
||||||
title = line.Text
|
|
||||||
}
|
|
||||||
if line.Verb == "TAG" {
|
|
||||||
event = line.Text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
date, channel, err := parseFilename(name)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := logs.Add(date, channel, title, event, "", false)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range logLines {
|
|
||||||
if line.Verb != "CHARS" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = posts.Add(l, l.Date, "chars", prefixReplacer.Replace(line.Args[0]), line.Text)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range logLines {
|
|
||||||
if line.Verb != "SCENE" && line.Verb != "ACTION" && line.Verb != "TEXT" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
postTime, err := time.ParseInLocation("2006-01-02 15:04:05", date.Format("2006-01-02")+" "+line.Args[1], time.Local)
|
|
||||||
diff := postTime.Sub(date)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if diff < 0 {
|
|
||||||
if diff > -time.Second {
|
|
||||||
postTime = postTime.Add(diff)
|
|
||||||
} else {
|
|
||||||
postTime = postTime.Add(time.Hour * 24)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.Args[0][0] == '=' {
|
|
||||||
line.Verb = "SCENE"
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = posts.Add(l, postTime, strings.ToLower(line.Verb), prefixReplacer.Replace(line.Args[0]), line.Text)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err = logs.UpdateCharacters(l)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(l.ID, "completed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/models/posts"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/logs"
|
|
||||||
)
|
|
||||||
|
|
||||||
var dateStr string
|
|
||||||
var tzStr string
|
|
||||||
var channel string
|
|
||||||
var fileName string
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var inputPosts []Post
|
|
||||||
|
|
||||||
flag.StringVar(&dateStr, "date", "2013-04-04", "Date in YYYY-MM-DD form.")
|
|
||||||
flag.StringVar(&tzStr, "tz", "Europe/Oslo", "Time zone for the date.")
|
|
||||||
flag.StringVar(&channel, "channel", "#RedrockAgency", "Channel it's set in.")
|
|
||||||
flag.StringVar(&fileName, "file", "-", "File to read from (- = stdin).")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
tz, err := time.LoadLocation(tzStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Parse timezone:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
date, err := time.ParseInLocation("2006-01-02", dateStr, tz)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Parse date:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Init store:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileName != "-" {
|
|
||||||
file, fileErr := os.Open(fileName)
|
|
||||||
if fileErr != nil {
|
|
||||||
log.Fatalln("Open file:", fileErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
inputPosts, err = parsePosts(file, date)
|
|
||||||
file.Close()
|
|
||||||
} else {
|
|
||||||
inputPosts, err = parsePosts(os.Stdin, date)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Read inputPosts:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(inputPosts) == 0 {
|
|
||||||
log.Fatalln("Input file contained no posts.")
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := logs.Add(inputPosts[0].Time, channel, "", "", "", false)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Add log:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Added log", l.ID)
|
|
||||||
|
|
||||||
for _, inputPost := range inputPosts {
|
|
||||||
p, err := posts.Add(l, inputPost.Time, inputPost.Kind, inputPost.Nick, inputPost.Text)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Add post:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Added post", p.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNotPost is returned by parsePost if the line is empty or not a post.
|
|
||||||
var ErrNotPost = errors.New("not a post")
|
|
||||||
|
|
||||||
// ErrTooShort is returned by parsePost if the line too short to be a post.
|
|
||||||
var ErrTooShort = errors.New("post too short")
|
|
||||||
|
|
||||||
// ErrInvalidTimestamp is returned by parsePost if the line is empty or not a post.
|
|
||||||
var ErrInvalidTimestamp = errors.New("invalid timestamp")
|
|
||||||
|
|
||||||
// A Post is a part of a log.
|
|
||||||
type Post struct {
|
|
||||||
Time time.Time
|
|
||||||
Kind string
|
|
||||||
Nick string
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePosts(reader io.Reader, date time.Time) ([]Post, error) {
|
|
||||||
prev := Post{}
|
|
||||||
posts := make([]Post, 0, 8)
|
|
||||||
bufReader := bufio.NewReader(reader)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := bufReader.ReadString('\n')
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(line) > 8 {
|
|
||||||
post, err := parsePost(strings.Trim(line, " \n\r"), date, prev)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
posts = append(posts, post)
|
|
||||||
prev = post
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return posts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePost(line string, date time.Time, prev Post) (Post, error) {
|
|
||||||
// Do basic validation
|
|
||||||
line = strings.Trim(line, " \t\n\r")
|
|
||||||
if len(line) == 0 || !strings.HasPrefix(line, "[") {
|
|
||||||
return Post{}, ErrNotPost
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse timestamp
|
|
||||||
tsEndIndex := strings.IndexByte(line, ']')
|
|
||||||
if tsEndIndex == -1 || len(line) < tsEndIndex+5 {
|
|
||||||
return Post{}, ErrNotPost
|
|
||||||
}
|
|
||||||
tsStr := line[1:tsEndIndex]
|
|
||||||
tsSplit := strings.Split(tsStr, ":")
|
|
||||||
tsUnits := make([]int, len(tsSplit))
|
|
||||||
if len(tsSplit) < 2 {
|
|
||||||
return Post{}, ErrNotPost
|
|
||||||
}
|
|
||||||
for i := range tsSplit {
|
|
||||||
n, err := strconv.Atoi(tsSplit[i])
|
|
||||||
if err != nil {
|
|
||||||
return Post{}, ErrNotPost
|
|
||||||
}
|
|
||||||
|
|
||||||
tsUnits[i] = n
|
|
||||||
}
|
|
||||||
if len(tsUnits) == 2 {
|
|
||||||
tsUnits = append(tsUnits, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
ts := time.Date(date.Year(), date.Month(), date.Day(), tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location())
|
|
||||||
if !prev.Time.IsZero() && prev.Time.After(ts) {
|
|
||||||
ts = ts.AddDate(0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if line[tsEndIndex+2] == '*' {
|
|
||||||
split := strings.SplitN(line[tsEndIndex+4:], " ", 2)
|
|
||||||
|
|
||||||
post := Post{
|
|
||||||
Time: ts,
|
|
||||||
Kind: "action",
|
|
||||||
Nick: strings.TrimLeft(split[0], "+@!~"),
|
|
||||||
Text: split[1],
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Nick[0] == '=' {
|
|
||||||
post.Kind = "scene"
|
|
||||||
}
|
|
||||||
|
|
||||||
return post, nil
|
|
||||||
} else if line[tsEndIndex+2] == '<' {
|
|
||||||
split := strings.SplitN(line[tsEndIndex+2:], " ", 2)
|
|
||||||
|
|
||||||
post := Post{
|
|
||||||
Time: ts,
|
|
||||||
Kind: "text",
|
|
||||||
Nick: strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"),
|
|
||||||
Text: split[1],
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Nick[0] == '=' {
|
|
||||||
post.Kind = "scene"
|
|
||||||
}
|
|
||||||
|
|
||||||
return post, nil
|
|
||||||
} else {
|
|
||||||
return Post{}, ErrNotPost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+78
-20
@@ -3,57 +3,115 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.aiterp.net/rpdata/api/space"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/logs"
|
"git.aiterp.net/rpdata/api/database"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/graph2"
|
"git.aiterp.net/rpdata/api/graph2"
|
||||||
"git.aiterp.net/rpdata/api/internal/auth"
|
"git.aiterp.net/rpdata/api/internal/config"
|
||||||
"git.aiterp.net/rpdata/api/internal/loader"
|
"git.aiterp.net/rpdata/api/internal/instrumentation"
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
"github.com/99designs/gqlgen/handler"
|
"github.com/99designs/gqlgen/handler"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := store.Init()
|
cfg := config.Global()
|
||||||
|
|
||||||
|
db, err := database.Init(cfg.Database)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to init store:", err)
|
log.Fatalln("Failed to init db:", err)
|
||||||
}
|
}
|
||||||
|
defer db.Close(context.Background())
|
||||||
|
|
||||||
|
var spaceClient *space.Client
|
||||||
|
if cfg.Space.Enabled {
|
||||||
|
spaceClient, err = space.Connect(cfg.Space)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to init space:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Space loaded")
|
||||||
|
} else {
|
||||||
|
log.Println("Space is disabled, file upload will not work!")
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceBundle := services.NewBundle(db, spaceClient)
|
||||||
|
|
||||||
|
instrumentation.Register()
|
||||||
|
|
||||||
http.Handle("/", handler.Playground("RPData API", "/graphql"))
|
http.Handle("/", handler.Playground("RPData API", "/graphql"))
|
||||||
http.Handle("/graphql", queryHandler())
|
http.Handle("/graphql", queryHandler(serviceBundle))
|
||||||
|
|
||||||
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := logs.RunFullUpdate()
|
err := serviceBundle.Logs.RefreshAllLogCharacters(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println("Refresh Characters:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Characters updated")
|
err = serviceBundle.Logs.FixImportDateBug(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Fix date bug:", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go logListedChanges(serviceBundle.Changes)
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":8081", nil))
|
log.Fatal(http.ListenAndServe(":8081", nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryHandler() http.HandlerFunc {
|
func logListedChanges(changes *services.ChangeService) {
|
||||||
|
sub := changes.Subscribe(context.Background(), models.ChangeFilter{})
|
||||||
|
|
||||||
|
for change := range sub {
|
||||||
|
log.Printf("Change: Author=%#+v Model=%#+v Op=%#+v", change.Author, change.Model, change.Op)
|
||||||
|
for _, object := range change.Objects() {
|
||||||
|
switch object := object.(type) {
|
||||||
|
case models.Log:
|
||||||
|
log.Printf("\tLog: %s (%s)", object.ID, object.ShortID)
|
||||||
|
case models.Character:
|
||||||
|
log.Printf("\tCharacter: %s", object.ID)
|
||||||
|
case models.Channel:
|
||||||
|
log.Printf("\tChannel: %s", object.Name)
|
||||||
|
case models.Post:
|
||||||
|
log.Printf("\tPost: %s", object.ID)
|
||||||
|
case models.Story:
|
||||||
|
log.Printf("\tStory: %s", object.ID)
|
||||||
|
case models.Tag:
|
||||||
|
log.Printf("\tTag: (%s,%s)", object.Kind, object.Name)
|
||||||
|
case models.Chapter:
|
||||||
|
log.Printf("\tChapter: %s", object.ID)
|
||||||
|
case models.Comment:
|
||||||
|
log.Printf("\tComment: %s", object.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Change subscription closed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryHandler(services *services.Bundle) http.HandlerFunc {
|
||||||
handler := handler.GraphQL(
|
handler := handler.GraphQL(
|
||||||
graph2.New(),
|
graph2.New(services),
|
||||||
|
handler.UploadMaxSize(10485760),
|
||||||
|
handler.UploadMaxMemory(1048576),
|
||||||
handler.RecoverFunc(func(ctx context.Context, err interface{}) error {
|
handler.RecoverFunc(func(ctx context.Context, err interface{}) error {
|
||||||
// send this panic somewhere
|
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
log.Println(string(debug.Stack()))
|
log.Println(string(debug.Stack()))
|
||||||
|
|
||||||
return fmt.Errorf("shit")
|
return fmt.Errorf("The server failed to serve your request due to an internal error, it has been logged")
|
||||||
}),
|
}),
|
||||||
|
handler.ComplexityLimit(300),
|
||||||
|
handler.RequestMiddleware(instrumentation.RequestMiddleware()),
|
||||||
|
handler.ResolverMiddleware(instrumentation.ResolverMiddleware()),
|
||||||
)
|
)
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
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") {
|
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().Set("X-Emerald-Herald", "Seek souls. Larger, more powerful souls.")
|
||||||
@@ -61,7 +119,7 @@ func queryHandler() http.HandlerFunc {
|
|||||||
w.Header().Add("X-Emerald-Herald", "Lest this land swallow you whole... As it has so many others.")
|
w.Header().Add("X-Emerald-Herald", "Lest this land swallow you whole... As it has so many others.")
|
||||||
}
|
}
|
||||||
|
|
||||||
r = auth.RequestWithToken(r)
|
r = services.Auth.RequestWithToken(r)
|
||||||
|
|
||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/config"
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/models/files"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
client := http.Client{Timeout: time.Second * 30}
|
|
||||||
|
|
||||||
res, err := client.Get(config.Global().Wiki.URL + "?action=query&list=allimages&ailimit=5&aiprop=dimensions|mime|url|user|timestamp&format=json&ailimit=500")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
log.Fatalln(res.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
data := ResponseData{}
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&data)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
store.Init()
|
|
||||||
|
|
||||||
for _, info := range data.Query.Allimages {
|
|
||||||
existing, err := files.FindName("wiki", info.Name, info.User)
|
|
||||||
if err == nil && existing.ID != "" {
|
|
||||||
log.Println("Skipping", info.Name, "because it already exists")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ts, err := time.Parse(time.RFC3339, info.Timestamp)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Skipping", info.Name, "error:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := files.Insert(info.Name, "wiki", info.Mime, info.User, ts, info.Size, info.URL)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Skipping", info.Name, "error:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(file.Name, "inserted ( id:", file.ID, ")")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The ResponseData from the wiki looks like this
|
|
||||||
type ResponseData struct {
|
|
||||||
Batchcomplete string `json:"batchcomplete"`
|
|
||||||
Query struct {
|
|
||||||
Allimages []struct {
|
|
||||||
Descriptionshorturl string `json:"descriptionshorturl"`
|
|
||||||
Descriptionurl string `json:"descriptionurl"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
Mime string `json:"mime"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Ns int `json:"ns"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Timestamp string `json:"timestamp"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
User string `json:"user"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
} `json:"allimages"`
|
|
||||||
} `json:"query"`
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/database/mongodb"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/config"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrDriverUnrecognized is returned if the driver is not recognized
|
||||||
|
var ErrDriverUnrecognized = errors.New("driver not recognized, check installed version or your configuration")
|
||||||
|
|
||||||
|
type Database interface {
|
||||||
|
Changes() repositories.ChangeRepository
|
||||||
|
Channels() repositories.ChannelRepository
|
||||||
|
Characters() repositories.CharacterRepository
|
||||||
|
Tags() repositories.TagRepository
|
||||||
|
Logs() repositories.LogRepository
|
||||||
|
Posts() repositories.PostRepository
|
||||||
|
Stories() repositories.StoryRepository
|
||||||
|
Chapters() repositories.ChapterRepository
|
||||||
|
Comments() repositories.CommentRepository
|
||||||
|
Keys() repositories.KeyRepository
|
||||||
|
Users() repositories.UserRepository
|
||||||
|
Files() repositories.FileRepository
|
||||||
|
|
||||||
|
Close(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init sets up the database.
|
||||||
|
func Init(config config.Database) (Database, error) {
|
||||||
|
switch config.Driver {
|
||||||
|
case "mongo", "mgo", "mongodb":
|
||||||
|
return mongodb.Init(config)
|
||||||
|
default:
|
||||||
|
return nil, ErrDriverUnrecognized
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type changeRepository struct {
|
||||||
|
changes *mgo.Collection
|
||||||
|
idCounter *counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) {
|
||||||
|
change := new(models.Change)
|
||||||
|
err := r.changes.FindId(id).One(change)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return change, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
limit := 0
|
||||||
|
if filter.EarliestDate != nil && !filter.EarliestDate.IsZero() {
|
||||||
|
query["date"] = bson.M{"$gte": *filter.EarliestDate}
|
||||||
|
}
|
||||||
|
if len(filter.Keys) > 0 {
|
||||||
|
query["keys"] = bson.M{"$in": filter.Keys}
|
||||||
|
} else {
|
||||||
|
query["listed"] = true
|
||||||
|
}
|
||||||
|
if filter.Author != nil && *filter.Author != "" {
|
||||||
|
query["author"] = *filter.Author
|
||||||
|
}
|
||||||
|
if filter.Limit != nil {
|
||||||
|
limit = *filter.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
initialSize := 64
|
||||||
|
if limit > 0 && limit < 256 {
|
||||||
|
initialSize = limit
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := make([]*models.Change, 0, initialSize)
|
||||||
|
err := r.changes.Find(query).Sort("-date").Limit(limit).All(&changes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) {
|
||||||
|
next, err := r.idCounter.Increment(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
change.ID = "Change_" + strconv.Itoa(next)
|
||||||
|
|
||||||
|
err = r.changes.Insert(&change)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &change, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *changeRepository) Remove(ctx context.Context, change models.Change) error {
|
||||||
|
return r.changes.RemoveId(change.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChangeRepository(db *mgo.Database) (repositories.ChangeRepository, error) {
|
||||||
|
collection := db.C("common.changes")
|
||||||
|
|
||||||
|
// Delete the old index if it exists.
|
||||||
|
_ = collection.DropIndexName("date_1")
|
||||||
|
|
||||||
|
err := collection.EnsureIndex(mgo.Index{
|
||||||
|
Name: "expiry",
|
||||||
|
Key: []string{"date"},
|
||||||
|
ExpireAfter: time.Hour * 2400, // 100 days
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.EnsureIndexKey("author")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.EnsureIndexKey("keys")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &changeRepository{
|
||||||
|
changes: collection,
|
||||||
|
idCounter: newCounter(db, "auto_increment", "Change"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newChannelRepository(db *mgo.Database) (*channelRepository, error) {
|
||||||
|
collection := db.C("common.channels")
|
||||||
|
|
||||||
|
err := collection.EnsureIndexKey("logged")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("event")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("location")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &channelRepository{
|
||||||
|
channels: collection,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type channelRepository struct {
|
||||||
|
channels *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *channelRepository) Find(ctx context.Context, name string) (*models.Channel, error) {
|
||||||
|
channel := new(models.Channel)
|
||||||
|
err := r.channels.FindId(name).One(channel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *channelRepository) List(ctx context.Context, filter models.ChannelFilter) ([]*models.Channel, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.Logged != nil {
|
||||||
|
query["logged"] = *filter.Logged
|
||||||
|
}
|
||||||
|
if filter.EventName != nil {
|
||||||
|
query["eventName"] = *filter.EventName
|
||||||
|
}
|
||||||
|
if filter.LocationName != nil {
|
||||||
|
query["locationName"] = *filter.LocationName
|
||||||
|
}
|
||||||
|
if len(filter.Names) > 0 {
|
||||||
|
query["_id"] = bson.M{"$in": filter.Names}
|
||||||
|
}
|
||||||
|
|
||||||
|
channels := make([]*models.Channel, 0, 32)
|
||||||
|
err := r.channels.Find(query).Limit(filter.Limit).Sort("_id").All(&channels)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *channelRepository) Insert(ctx context.Context, channel models.Channel) (*models.Channel, error) {
|
||||||
|
err := r.channels.Insert(channel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &channel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *channelRepository) Update(ctx context.Context, channel models.Channel, update models.ChannelUpdate) (*models.Channel, error) {
|
||||||
|
updateBson := bson.M{}
|
||||||
|
if update.Logged != nil {
|
||||||
|
updateBson["logged"] = *update.Logged
|
||||||
|
channel.Logged = *update.Logged
|
||||||
|
}
|
||||||
|
if update.LocationName != nil {
|
||||||
|
updateBson["locationName"] = *update.LocationName
|
||||||
|
channel.LocationName = *update.LocationName
|
||||||
|
}
|
||||||
|
if update.EventName != nil {
|
||||||
|
updateBson["eventName"] = *update.EventName
|
||||||
|
channel.EventName = *update.EventName
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.channels.UpdateId(channel.Name, bson.M{"$set": updateBson})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &channel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *channelRepository) Remove(ctx context.Context, channel models.Channel) error {
|
||||||
|
return r.channels.RemoveId(channel.Name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/generate"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chapterRepository struct {
|
||||||
|
chapters *mgo.Collection
|
||||||
|
comments *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChapterRepository(db *mgo.Database) (repositories.ChapterRepository, error) {
|
||||||
|
collection := db.C("story.chapters")
|
||||||
|
|
||||||
|
err := collection.EnsureIndexKey("storyId")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("author")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("createdDate")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chapterRepository{
|
||||||
|
chapters: collection,
|
||||||
|
comments: db.C("story.comments"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chapterRepository) Find(ctx context.Context, id string) (*models.Chapter, error) {
|
||||||
|
chapter := new(models.Chapter)
|
||||||
|
err := r.chapters.FindId(id).One(chapter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.StoryID != nil {
|
||||||
|
query["storyId"] = *filter.StoryID
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters := make([]*models.Chapter, 0, 32)
|
||||||
|
err := r.chapters.Find(query).Sort("createdDate").Limit(filter.Limit).All(&chapters)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return chapters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) {
|
||||||
|
chapter.ID = generate.StoryID()
|
||||||
|
|
||||||
|
err := r.chapters.Insert(chapter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chapterRepository) Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) {
|
||||||
|
updateBson := bson.M{}
|
||||||
|
if update.Title != nil {
|
||||||
|
updateBson["title"] = *update.Title
|
||||||
|
chapter.Title = *update.Title
|
||||||
|
}
|
||||||
|
if update.Source != nil {
|
||||||
|
updateBson["source"] = *update.Source
|
||||||
|
chapter.Source = *update.Source
|
||||||
|
}
|
||||||
|
if update.FictionalDate != nil {
|
||||||
|
updateBson["fictionalDate"] = *update.FictionalDate
|
||||||
|
chapter.FictionalDate = *update.FictionalDate
|
||||||
|
}
|
||||||
|
if update.CommentMode != nil {
|
||||||
|
updateBson["commentMode"] = *update.CommentMode
|
||||||
|
chapter.CommentMode = *update.CommentMode
|
||||||
|
}
|
||||||
|
if update.CommentsLocked != nil {
|
||||||
|
updateBson["commentsLocked"] = *update.CommentsLocked
|
||||||
|
chapter.CommentsLocked = *update.CommentsLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": updateBson})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chapterRepository) Move(ctx context.Context, chapter models.Chapter, from, to models.Story) (*models.Chapter, error) {
|
||||||
|
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": bson.M{"storyId": to.ID}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.StoryID = to.ID
|
||||||
|
|
||||||
|
return &chapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chapterRepository) Delete(ctx context.Context, chapter models.Chapter) error {
|
||||||
|
err := r.chapters.RemoveId(chapter.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := r.comments.RemoveAll(bson.M{"chapterId": chapter.ID})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to remove comments:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Removed chapter %s (%d comments)", chapter.ID, c.Removed)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
type characterRepository struct {
|
||||||
|
characters *mgo.Collection
|
||||||
|
cidCounter *counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCharacterRepository(db *mgo.Database) (repositories.CharacterRepository, error) {
|
||||||
|
collection := db.C("common.characters")
|
||||||
|
|
||||||
|
err := collection.EnsureIndexKey("name")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.EnsureIndexKey("shortName")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.EnsureIndexKey("author")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.EnsureIndex(mgo.Index{
|
||||||
|
Key: []string{"nicks"},
|
||||||
|
Unique: true,
|
||||||
|
DropDups: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.EnsureIndex(mgo.Index{
|
||||||
|
Key: []string{"$text:description"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &characterRepository{
|
||||||
|
characters: collection,
|
||||||
|
cidCounter: newCounter(db, "auto_increment", "Character"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) Find(ctx context.Context, id string) (*models.Character, error) {
|
||||||
|
character := new(models.Character)
|
||||||
|
err := r.characters.FindId(id).One(character)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return character, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) FindNick(ctx context.Context, nick string) (*models.Character, error) {
|
||||||
|
character := new(models.Character)
|
||||||
|
err := r.characters.Find(bson.M{"nick": nick}).One(character)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return character, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) FindName(ctx context.Context, name string) (*models.Character, error) {
|
||||||
|
query := bson.M{
|
||||||
|
"$or": []bson.M{
|
||||||
|
{"shortName": name},
|
||||||
|
{"name": name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for all characters matching query
|
||||||
|
characters := make([]*models.Character, 0, 8)
|
||||||
|
err := r.characters.Find(query).All(&characters)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
} else if len(characters) == 0 {
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize exact match
|
||||||
|
for _, character := range characters {
|
||||||
|
if character.Name == name {
|
||||||
|
return character, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return characters[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) List(ctx context.Context, filter models.CharacterFilter) ([]*models.Character, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.Author != nil {
|
||||||
|
query["author"] = *filter.Author
|
||||||
|
}
|
||||||
|
if len(filter.IDs) > 0 {
|
||||||
|
query["_id"] = bson.M{"$in": filter.IDs}
|
||||||
|
}
|
||||||
|
if len(filter.Nicks) > 0 {
|
||||||
|
query["nicks"] = bson.M{"$in": filter.Nicks}
|
||||||
|
}
|
||||||
|
if len(filter.Names) > 0 {
|
||||||
|
query["$or"] = []bson.M{
|
||||||
|
{"name": bson.M{"$in": filter.Names}},
|
||||||
|
{"shortName": bson.M{"$in": filter.Names}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filter.Search != nil {
|
||||||
|
query["$text"] = bson.M{"$search": *filter.Search}
|
||||||
|
}
|
||||||
|
|
||||||
|
characters := make([]*models.Character, 0, 32)
|
||||||
|
err := r.characters.Find(query).Limit(filter.Limit).All(&characters)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return characters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(characters, func(i, j int) bool {
|
||||||
|
ni, _ := strconv.Atoi(characters[i].ID[1:])
|
||||||
|
nj, _ := strconv.Atoi(characters[j].ID[1:])
|
||||||
|
|
||||||
|
return ni < nj
|
||||||
|
})
|
||||||
|
|
||||||
|
return characters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) Insert(ctx context.Context, character models.Character) (*models.Character, error) {
|
||||||
|
nextId, err := r.cidCounter.Increment(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
character.ID = "C" + strconv.Itoa(nextId)
|
||||||
|
|
||||||
|
err = r.characters.Insert(&character)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &character, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) Update(ctx context.Context, character models.Character, update models.CharacterUpdate) (*models.Character, error) {
|
||||||
|
updateBson := bson.M{}
|
||||||
|
if update.Name != nil {
|
||||||
|
updateBson["name"] = *update.Name
|
||||||
|
character.Name = *update.Name
|
||||||
|
}
|
||||||
|
if update.ShortName != nil {
|
||||||
|
updateBson["shortName"] = *update.ShortName
|
||||||
|
character.ShortName = *update.ShortName
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
updateBson["description"] = *update.Description
|
||||||
|
character.Description = *update.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.characters.UpdateId(character.ID, bson.M{"$set": updateBson})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &character, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) AddNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
|
||||||
|
if character.HasNick(nick) {
|
||||||
|
return nil, errors.New("nick already exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
match := bson.M{
|
||||||
|
"_id": character.ID,
|
||||||
|
"nicks": bson.M{"$ne": nick},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.characters.Update(match, bson.M{"$push": bson.M{"nicks": nick}})
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
|
||||||
|
copy(newNicks, character.Nicks)
|
||||||
|
character.Nicks = append(newNicks, nick)
|
||||||
|
|
||||||
|
return &character, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) RemoveNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
|
||||||
|
if !character.HasNick(nick) {
|
||||||
|
return nil, errors.New("nick does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
match := bson.M{
|
||||||
|
"_id": character.ID,
|
||||||
|
"nicks": nick,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.characters.Update(match, bson.M{"$pull": bson.M{"nicks": nick}})
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
} else if mErr, ok := err.(*mgo.LastError); ok && mErr.Code == 11000 {
|
||||||
|
return nil, errors.New("The nick belongs to another character already")
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
|
||||||
|
copy(newNicks, character.Nicks)
|
||||||
|
for i := range newNicks {
|
||||||
|
if newNicks[i] == nick {
|
||||||
|
newNicks = append(newNicks[:i], newNicks[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
character.Nicks = newNicks
|
||||||
|
|
||||||
|
return &character, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *characterRepository) Delete(ctx context.Context, character models.Character) error {
|
||||||
|
return r.characters.RemoveId(character.ID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/generate"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commentRepository struct {
|
||||||
|
comments *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommentRepository(db *mgo.Database) (repositories.CommentRepository, error) {
|
||||||
|
collection := db.C("story.comments")
|
||||||
|
|
||||||
|
err := collection.EnsureIndexKey("chapterId")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("author")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("createdDate")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &commentRepository{
|
||||||
|
comments: collection,
|
||||||
|
}
|
||||||
|
|
||||||
|
go r.fixFieldTypo()
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *commentRepository) Find(ctx context.Context, id string) (*models.Comment, error) {
|
||||||
|
comment := new(models.Comment)
|
||||||
|
err := r.comments.FindId(id).One(comment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *commentRepository) List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.ChapterID != nil {
|
||||||
|
query["chapterId"] = *filter.ChapterID
|
||||||
|
}
|
||||||
|
|
||||||
|
comments := make([]*models.Comment, 0, 32)
|
||||||
|
err := r.comments.Find(query).Sort("createdDate").Limit(filter.Limit).All(&comments)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return comments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *commentRepository) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) {
|
||||||
|
comment.ID = generate.CommentID()
|
||||||
|
|
||||||
|
err := r.comments.Insert(comment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *commentRepository) Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Comment, error) {
|
||||||
|
updateBson := bson.M{}
|
||||||
|
if update.Subject != nil {
|
||||||
|
updateBson["subject"] = *update.Subject
|
||||||
|
comment.Subject = *update.Subject
|
||||||
|
}
|
||||||
|
if update.Source != nil {
|
||||||
|
updateBson["source"] = *update.Source
|
||||||
|
comment.Source = *update.Source
|
||||||
|
}
|
||||||
|
if update.FictionalDate != nil {
|
||||||
|
updateBson["fictionalDate"] = *update.FictionalDate
|
||||||
|
comment.FictionalDate = *update.FictionalDate
|
||||||
|
}
|
||||||
|
if update.CharacterID != nil {
|
||||||
|
updateBson["characterId"] = *update.CharacterID
|
||||||
|
comment.CharacterID = *update.CharacterID
|
||||||
|
}
|
||||||
|
if update.CharacterName != nil {
|
||||||
|
updateBson["characterName"] = *update.CharacterName
|
||||||
|
comment.CharacterName = *update.CharacterName
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.comments.UpdateId(comment.ID, bson.M{"$set": updateBson})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *commentRepository) Delete(ctx context.Context, comment models.Comment) error {
|
||||||
|
return r.comments.RemoveId(comment.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *commentRepository) fixFieldTypo() {
|
||||||
|
c, err := r.comments.UpdateAll(bson.M{
|
||||||
|
"editeddDate": bson.M{"$ne": nil},
|
||||||
|
}, bson.M{
|
||||||
|
"$rename": bson.M{"editeddDate": "editedDate"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Failed to run name typo fix:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Updated > 0 {
|
||||||
|
log.Println("Fixed editeddDate field name typo in", c.Updated, "comments")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/internal/config"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MongoDB struct {
|
||||||
|
session *mgo.Session
|
||||||
|
|
||||||
|
changes repositories.ChangeRepository
|
||||||
|
channels *channelRepository
|
||||||
|
characters repositories.CharacterRepository
|
||||||
|
tags repositories.TagRepository
|
||||||
|
logs *logRepository
|
||||||
|
posts *postRepository
|
||||||
|
files *fileRepository
|
||||||
|
stories repositories.StoryRepository
|
||||||
|
chapters repositories.ChapterRepository
|
||||||
|
comments repositories.CommentRepository
|
||||||
|
keys repositories.KeyRepository
|
||||||
|
users repositories.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Changes() repositories.ChangeRepository {
|
||||||
|
return m.changes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Characters() repositories.CharacterRepository {
|
||||||
|
return m.characters
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Tags() repositories.TagRepository {
|
||||||
|
return m.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Logs() repositories.LogRepository {
|
||||||
|
return m.logs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Posts() repositories.PostRepository {
|
||||||
|
return m.posts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Channels() repositories.ChannelRepository {
|
||||||
|
return m.channels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Files() repositories.FileRepository {
|
||||||
|
return m.files
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Stories() repositories.StoryRepository {
|
||||||
|
return m.stories
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Chapters() repositories.ChapterRepository {
|
||||||
|
return m.chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Comments() repositories.CommentRepository {
|
||||||
|
return m.comments
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Keys() repositories.KeyRepository {
|
||||||
|
return m.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Users() repositories.UserRepository {
|
||||||
|
return m.users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Close(ctx context.Context) error {
|
||||||
|
m.session.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the mongodb database
|
||||||
|
func Init(cfg config.Database) (*MongoDB, error) {
|
||||||
|
port := cfg.Port
|
||||||
|
if port <= 0 {
|
||||||
|
port = 27017
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := mgo.DialWithInfo(&mgo.DialInfo{
|
||||||
|
Addrs: []string{fmt.Sprintf("%s:%d", cfg.Host, port)},
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Database: cfg.Db,
|
||||||
|
Username: cfg.Username,
|
||||||
|
Password: cfg.Password,
|
||||||
|
Mechanism: cfg.Mechanism,
|
||||||
|
Source: cfg.Db,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := session.DB(cfg.Db)
|
||||||
|
|
||||||
|
characters, err := newCharacterRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channels, err := newChannelRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
changes, err := newChangeRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := newLogRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := newPostRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := newFileRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stories, err := newStoryRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters, err := newChapterRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, err := newCommentRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err := newKeyRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := newUserRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go posts.fixPositions(logs)
|
||||||
|
|
||||||
|
return &MongoDB{
|
||||||
|
session: session,
|
||||||
|
|
||||||
|
changes: changes,
|
||||||
|
characters: characters,
|
||||||
|
channels: channels,
|
||||||
|
tags: newTagRepository(db),
|
||||||
|
stories: stories,
|
||||||
|
chapters: chapters,
|
||||||
|
comments: comments,
|
||||||
|
logs: logs,
|
||||||
|
posts: posts,
|
||||||
|
files: files,
|
||||||
|
keys: keys,
|
||||||
|
users: users,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type counter struct {
|
||||||
|
coll *mgo.Collection
|
||||||
|
category string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCounter(db *mgo.Database, category, name string) *counter {
|
||||||
|
return &counter{
|
||||||
|
coll: db.C("core.counters"),
|
||||||
|
category: category,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *counter) WithName(name string) *counter {
|
||||||
|
return &counter{
|
||||||
|
coll: c.coll,
|
||||||
|
category: c.category,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *counter) WithCategory(category string) *counter {
|
||||||
|
return &counter{
|
||||||
|
coll: c.coll,
|
||||||
|
category: category,
|
||||||
|
name: c.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *counter) With(category, name string) *counter {
|
||||||
|
return &counter{
|
||||||
|
coll: c.coll,
|
||||||
|
category: category,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *counter) Increment(amount int) (int, error) {
|
||||||
|
type counterDoc struct {
|
||||||
|
ID string `bson:"_id"`
|
||||||
|
Value int `bson:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.category + "." + c.name
|
||||||
|
doc := counterDoc{}
|
||||||
|
|
||||||
|
_, err := c.coll.Find(bson.M{"_id": id}).Apply(mgo.Change{
|
||||||
|
Update: bson.M{"$inc": bson.M{"value": amount}},
|
||||||
|
Upsert: true,
|
||||||
|
ReturnNew: true,
|
||||||
|
}, &doc)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.Value, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/generate"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileRepository struct {
|
||||||
|
files *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fileRepository) Find(ctx context.Context, id string) (*models.File, error) {
|
||||||
|
file := new(models.File)
|
||||||
|
err := r.files.FindId(id).One(file)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fileRepository) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.Author != nil {
|
||||||
|
query["author"] = *filter.Author
|
||||||
|
}
|
||||||
|
if filter.MimeTypes != nil {
|
||||||
|
query["mimeType"] = bson.M{"$in": filter.MimeTypes}
|
||||||
|
}
|
||||||
|
if filter.Public != nil {
|
||||||
|
query["public"] = *filter.Public
|
||||||
|
}
|
||||||
|
|
||||||
|
files := make([]*models.File, 0, 16)
|
||||||
|
err := r.files.Find(query).Sort("-date").All(&files)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fileRepository) Insert(ctx context.Context, file models.File) (*models.File, error) {
|
||||||
|
file.ID = generate.FileID()
|
||||||
|
|
||||||
|
err := r.files.Insert(&file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fileRepository) Update(ctx context.Context, file models.File, update models.FileUpdate) (*models.File, error) {
|
||||||
|
updateBson := bson.M{}
|
||||||
|
if update.Public != nil {
|
||||||
|
updateBson["public"] = *update.Public
|
||||||
|
file.Public = *update.Public
|
||||||
|
}
|
||||||
|
if update.Name != nil {
|
||||||
|
updateBson["name"] = *update.Name
|
||||||
|
file.Name = *update.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.files.UpdateId(file.ID, bson.M{"$set": updateBson})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fileRepository) Delete(ctx context.Context, file models.File) error {
|
||||||
|
return r.files.RemoveId(file.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileRepository(db *mgo.Database) (*fileRepository, error) {
|
||||||
|
files := db.C("file.headers")
|
||||||
|
|
||||||
|
err := files.EnsureIndexKey("author")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = files.EnsureIndexKey("mimeType")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = files.EnsureIndexKey("public")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fileRepository{files: files}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/generate"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type keyRepository struct {
|
||||||
|
keys *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newKeyRepository(db *mgo.Database) (repositories.KeyRepository, error) {
|
||||||
|
collection := db.C("auth.keys")
|
||||||
|
|
||||||
|
err := collection.EnsureIndexKey("user")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keyRepository{keys: collection}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *keyRepository) Find(ctx context.Context, id string) (*models.Key, error) {
|
||||||
|
key := models.Key{}
|
||||||
|
err := r.keys.FindId(id).One(&key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &key, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *keyRepository) List(ctx context.Context, filter models.KeyFilter) ([]*models.Key, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.UserID != nil {
|
||||||
|
query["user"] = *filter.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]*models.Key, 0, 4)
|
||||||
|
err := r.keys.Find(query).All(&keys)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *keyRepository) Insert(ctx context.Context, key models.Key) (*models.Key, error) {
|
||||||
|
key.ID = generate.KeyID()
|
||||||
|
|
||||||
|
err := r.keys.Insert(&key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *keyRepository) Delete(ctx context.Context, key models.Key) error {
|
||||||
|
return r.keys.RemoveId(key.ID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/generate"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logRepository struct {
|
||||||
|
openMutex sync.Mutex
|
||||||
|
|
||||||
|
logs *mgo.Collection
|
||||||
|
posts *mgo.Collection
|
||||||
|
shortIdCounter *counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogRepository(db *mgo.Database) (*logRepository, error) {
|
||||||
|
logs := db.C("logbot3.logs")
|
||||||
|
posts := db.C("logbot3.posts")
|
||||||
|
|
||||||
|
err := logs.EnsureIndexKey("date")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = logs.EnsureIndexKey("channel")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = logs.EnsureIndexKey("characterIds")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = logs.EnsureIndexKey("event")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = logs.EnsureIndex(mgo.Index{Key: []string{"channel", "open"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = logs.EnsureIndex(mgo.Index{
|
||||||
|
Key: []string{"shortId"},
|
||||||
|
Unique: true,
|
||||||
|
DropDups: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logRepository{
|
||||||
|
logs: logs,
|
||||||
|
posts: posts,
|
||||||
|
shortIdCounter: newCounter(db, "auto_increment", "Log"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) {
|
||||||
|
log := new(models.Log)
|
||||||
|
err := r.logs.Find(bson.M{"$or": []bson.M{{"_id": id}, {"shortId": id}}}).One(log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.Search != nil {
|
||||||
|
searchQuery := bson.M{
|
||||||
|
"$text": bson.M{"$search": *filter.Search},
|
||||||
|
"logId": bson.M{"$ne": nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
logIds := make([]string, 0, 64)
|
||||||
|
err := r.posts.Find(searchQuery).Distinct("logId", &logIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query["shortId"] = bson.M{"$in": logIds}
|
||||||
|
}
|
||||||
|
if filter.Open != nil {
|
||||||
|
r.openMutex.Lock()
|
||||||
|
defer r.openMutex.Unlock()
|
||||||
|
|
||||||
|
query["open"] = filter.Open
|
||||||
|
}
|
||||||
|
if len(filter.Characters) > 0 {
|
||||||
|
query["characterIds"] = bson.M{"$all": filter.Characters}
|
||||||
|
}
|
||||||
|
if len(filter.Channels) > 0 {
|
||||||
|
query["channel"] = bson.M{"$in": filter.Channels}
|
||||||
|
}
|
||||||
|
if len(filter.Events) > 0 {
|
||||||
|
query["event"] = bson.M{"$in": filter.Events}
|
||||||
|
}
|
||||||
|
if filter.MinDate != nil && filter.MaxDate != nil {
|
||||||
|
query["date"] = bson.M{"$gte": *filter.MinDate, "$lte": *filter.MaxDate}
|
||||||
|
} else if filter.MinDate != nil {
|
||||||
|
query["date"] = bson.M{"$gte": *filter.MinDate}
|
||||||
|
} else if filter.MaxDate != nil {
|
||||||
|
query["date"] = bson.M{"$lt": *filter.MaxDate}
|
||||||
|
}
|
||||||
|
|
||||||
|
logs := make([]*models.Log, 0, 32)
|
||||||
|
err := r.logs.Find(query).Sort("-date").Limit(filter.Limit).All(&logs)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return logs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) Insert(ctx context.Context, log models.Log) (*models.Log, error) {
|
||||||
|
nextShortId, err := r.shortIdCounter.Increment(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.ID = generate.LogID(log)
|
||||||
|
log.ShortID = "L" + strconv.Itoa(nextShortId)
|
||||||
|
|
||||||
|
if log.Open {
|
||||||
|
// There can be only one open log in the same channel.
|
||||||
|
r.openMutex.Lock()
|
||||||
|
defer r.openMutex.Unlock()
|
||||||
|
|
||||||
|
_, err = r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Cannot close other logs: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.logs.Insert(&log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) Update(ctx context.Context, log models.Log, update models.LogUpdate) (*models.Log, error) {
|
||||||
|
updateBson := bson.M{}
|
||||||
|
if update.Open != nil {
|
||||||
|
if *update.Open == true {
|
||||||
|
// There can be only one open log in the same channel.
|
||||||
|
r.openMutex.Lock()
|
||||||
|
defer r.openMutex.Unlock()
|
||||||
|
|
||||||
|
_, err := r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Cannot close other logs: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBson["open"] = *update.Open
|
||||||
|
log.Open = *update.Open
|
||||||
|
}
|
||||||
|
if update.Title != nil {
|
||||||
|
updateBson["title"] = *update.Title
|
||||||
|
log.Title = *update.Title
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
updateBson["description"] = *update.Description
|
||||||
|
log.Description = *update.Description
|
||||||
|
}
|
||||||
|
if update.EventName != nil {
|
||||||
|
updateBson["event"] = *update.EventName
|
||||||
|
log.EventName = *update.EventName
|
||||||
|
}
|
||||||
|
if update.CharacterIDs != nil {
|
||||||
|
updateBson["characterIds"] = update.CharacterIDs
|
||||||
|
log.CharacterIDs = update.CharacterIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.logs.UpdateId(log.ID, bson.M{"$set": updateBson})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logRepository) Delete(ctx context.Context, log models.Log) error {
|
||||||
|
err := r.logs.RemoveId(log.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = r.posts.RemoveAll(bson.M{"logId": log.ShortID})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/generate"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postRepository struct {
|
||||||
|
logs *mgo.Collection
|
||||||
|
posts *mgo.Collection
|
||||||
|
|
||||||
|
orderMutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPostRepository(db *mgo.Database) (*postRepository, error) {
|
||||||
|
posts := db.C("logbot3.posts")
|
||||||
|
|
||||||
|
err := posts.EnsureIndexKey("logId")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = posts.EnsureIndexKey("time")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = posts.EnsureIndexKey("kind")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = posts.EnsureIndexKey("position")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = posts.EnsureIndex(mgo.Index{
|
||||||
|
Key: []string{"$text:text"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &postRepository{
|
||||||
|
posts: posts,
|
||||||
|
logs: db.C("logbot3.logs"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *postRepository) Find(ctx context.Context, id string) (*models.Post, error) {
|
||||||
|
post := new(models.Post)
|
||||||
|
err := r.posts.FindId(id).One(post)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *postRepository) List(ctx context.Context, filter models.PostFilter) ([]*models.Post, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.LogID != nil && *filter.LogID != "" {
|
||||||
|
logId := *filter.LogID
|
||||||
|
if !strings.HasPrefix(logId, "L") {
|
||||||
|
// Resolve long id to short id
|
||||||
|
log := new(models.Log)
|
||||||
|
err := r.logs.FindId(logId).Select(bson.M{"logId": 1, "_id": 1}).One(log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logId = log.ShortID
|
||||||
|
}
|
||||||
|
|
||||||
|
query["logId"] = logId
|
||||||
|
}
|
||||||
|
if len(filter.IDs) > 0 {
|
||||||
|
query["_id"] = bson.M{"$in": filter.IDs}
|
||||||
|
}
|
||||||
|
if len(filter.Kinds) > 0 {
|
||||||
|
query["kind"] = bson.M{"$in": filter.Kinds}
|
||||||
|
}
|
||||||
|
if filter.Search != nil {
|
||||||
|
query["$text"] = bson.M{"$search": *filter.Search}
|
||||||
|
}
|
||||||
|
|
||||||
|
posts := make([]*models.Post, 0, 32)
|
||||||
|
err := r.posts.Find(query).Sort("-logId", "position").Limit(filter.Limit).All(&posts)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return []*models.Post{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *postRepository) Insert(ctx context.Context, post models.Post) (*models.Post, error) {
|
||||||
|
r.orderMutex.Lock()
|
||||||
|
defer r.orderMutex.Unlock()
|
||||||
|
|
||||||
|
lastPost := new(models.Post)
|
||||||
|
err := r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
|
||||||
|
if err != nil && err != mgo.ErrNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
post.ID = generate.PostID()
|
||||||
|
post.Position = lastPost.Position + 1 // Position 1 is first position, so this is safe.
|
||||||
|
|
||||||
|
err = r.posts.Insert(post)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *postRepository) InsertMany(ctx context.Context, posts ...*models.Post) ([]*models.Post, error) {
|
||||||
|
if len(posts) == 0 {
|
||||||
|
return []*models.Post{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logId := posts[0].LogID
|
||||||
|
for _, post := range posts {
|
||||||
|
if post.LogID != logId {
|
||||||
|
return nil, repositories.ErrParentMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
post.ID = generate.PostID()
|
||||||
|
}
|
||||||
|
|
||||||
|
r.orderMutex.Lock()
|
||||||
|
defer r.orderMutex.Unlock()
|
||||||
|
|
||||||
|
lastPost := new(models.Post)
|
||||||
|
err := r.posts.Find(bson.M{"logId": posts[0].LogID}).Sort("-position").One(lastPost)
|
||||||
|
if err != nil && err != mgo.ErrNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
docs := make([]interface{}, len(posts))
|
||||||
|
for i := range posts {
|
||||||
|
posts[i].Position = lastPost.Position + 1 + i
|
||||||
|
docs[i] = posts[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.posts.Insert(docs...)
|
||||||
|
if err != nil && err != mgo.ErrNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *postRepository) Update(ctx context.Context, post models.Post, update models.PostUpdate) (*models.Post, error) {
|
||||||
|
updateBson := bson.M{}
|
||||||
|
if update.Time != nil {
|
||||||
|
updateBson["time"] = *update.Time
|
||||||
|
post.Time = *update.Time
|
||||||
|
}
|
||||||
|
if update.Kind != nil {
|
||||||
|
updateBson["kind"] = *update.Kind
|
||||||
|
post.Kind = *update.Kind
|
||||||
|
}
|
||||||
|
if update.Nick != nil {
|
||||||
|
updateBson["nick"] = *update.Nick
|
||||||
|
post.Nick = *update.Nick
|
||||||
|
}
|
||||||
|
if update.Text != nil {
|
||||||
|
updateBson["text"] = *update.Text
|
||||||
|
post.Text = *update.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.posts.UpdateId(post.ID, bson.M{"$set": updateBson})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *postRepository) Move(ctx context.Context, post models.Post, position int) ([]*models.Post, error) {
|
||||||
|
// If only MongoDB transactions weren't awful, this function would have been safe.
|
||||||
|
// Since it isn't, then good luck.
|
||||||
|
|
||||||
|
// Validate lower position bound.
|
||||||
|
if position < 1 {
|
||||||
|
return nil, repositories.ErrInvalidPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the operations on adjacent posts
|
||||||
|
var resultFilter bson.M
|
||||||
|
var pushFilter bson.M
|
||||||
|
var increment int
|
||||||
|
if post.Position > position {
|
||||||
|
pushFilter = bson.M{"position": bson.M{
|
||||||
|
"$gte": position,
|
||||||
|
"$lt": post.Position,
|
||||||
|
}}
|
||||||
|
increment = 1
|
||||||
|
resultFilter = bson.M{"position": bson.M{
|
||||||
|
"$lte": post.Position,
|
||||||
|
"$gte": position,
|
||||||
|
}}
|
||||||
|
} else {
|
||||||
|
pushFilter = bson.M{"position": bson.M{
|
||||||
|
"$lte": position,
|
||||||
|
"$gt": post.Position,
|
||||||
|
}}
|
||||||
|
increment = -1
|
||||||
|
resultFilter = bson.M{"position": bson.M{
|
||||||
|
"$lte": position,
|
||||||
|
"$gte": post.Position,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
pushFilter["logId"] = post.LogID
|
||||||
|
resultFilter["logId"] = post.LogID
|
||||||
|
|
||||||
|
// From here on out, sync is required
|
||||||
|
r.orderMutex.Lock()
|
||||||
|
defer r.orderMutex.Unlock()
|
||||||
|
|
||||||
|
// Detect ninja shenanigans
|
||||||
|
post2 := new(models.Post)
|
||||||
|
err := r.posts.FindId(post.ID).One(post2)
|
||||||
|
if err != nil && post2.Position != post.Position {
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate upper position bound
|
||||||
|
lastPost := new(models.Post)
|
||||||
|
err = r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
|
||||||
|
if err != nil && err != mgo.ErrNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if position > lastPost.Position {
|
||||||
|
return nil, repositories.ErrInvalidPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the posts
|
||||||
|
changeInfo, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": increment}})
|
||||||
|
if err != nil && err != mgo.ErrNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": position}})
|
||||||
|
if err != nil {
|
||||||
|
// Try to undo it
|
||||||
|
_, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": -increment}})
|
||||||
|
if err != nil && err != mgo.ErrNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]*models.Post, 0, changeInfo.Matched+1)
|
||||||
|
err = r.posts.Find(resultFilter).All(&results)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *postRepository) Delete(ctx context.Context, post models.Post) error {
|
||||||
|
r.orderMutex.Lock()
|
||||||
|
defer r.orderMutex.Unlock()
|
||||||
|
|
||||||
|
err := r.posts.RemoveId(post.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = r.posts.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *postRepository) fixPositions(logRepo *logRepository) {
|
||||||
|
disorders := make([]int, 0, 16)
|
||||||
|
diffs := make([]int, 0, 16)
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
timeout, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
logs, err := logRepo.List(timeout, models.LogFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get logs for position fix:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Starting log position fixing, this should not take longer than 10 seconds.")
|
||||||
|
|
||||||
|
for _, l := range logs {
|
||||||
|
r.orderMutex.Lock()
|
||||||
|
|
||||||
|
posts, err := r.List(timeout, models.PostFilter{LogID: &l.ShortID})
|
||||||
|
if err != nil {
|
||||||
|
r.orderMutex.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
disorders = disorders[:0]
|
||||||
|
diffs = diffs[:0]
|
||||||
|
for i, post := range posts {
|
||||||
|
if post.Position != (i + 1) {
|
||||||
|
disorders = append(disorders, i)
|
||||||
|
diffs = append(diffs, post.Position-(i+1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(disorders) > 0 {
|
||||||
|
log.Println(len(disorders), "order errors detected in", l.ID)
|
||||||
|
|
||||||
|
ops := 0
|
||||||
|
|
||||||
|
for i, post := range posts {
|
||||||
|
if (i + 1) != posts[i].Position {
|
||||||
|
ops++
|
||||||
|
|
||||||
|
err := r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": i + 1}})
|
||||||
|
if err != nil {
|
||||||
|
log.Println(l.ShortID, "fix failed after", ops, "ops:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(l.ShortID, "fixed after", ops, "ops.")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.orderMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Log position fixing finished in", time.Since(startTime))
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/generate"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type storyRepository struct {
|
||||||
|
stories *mgo.Collection
|
||||||
|
chapters *mgo.Collection
|
||||||
|
comments *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStoryRepository(db *mgo.Database) (repositories.StoryRepository, error) {
|
||||||
|
collection := db.C("story.stories")
|
||||||
|
|
||||||
|
err := collection.EnsureIndexKey("tags")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("author")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("updatedDate")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("fictionalDate")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = collection.EnsureIndexKey("listed")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &storyRepository{
|
||||||
|
stories: collection,
|
||||||
|
chapters: db.C("story.chapters"),
|
||||||
|
comments: db.C("story.comments"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyRepository) Find(ctx context.Context, id string) (*models.Story, error) {
|
||||||
|
story := new(models.Story)
|
||||||
|
err := r.stories.FindId(id).One(story)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return story, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) {
|
||||||
|
query := bson.M{}
|
||||||
|
if filter.Author != nil {
|
||||||
|
query["author"] = *filter.Author
|
||||||
|
}
|
||||||
|
if filter.Category != nil {
|
||||||
|
query["category"] = *filter.Category
|
||||||
|
}
|
||||||
|
if filter.Open != nil {
|
||||||
|
query["open"] = *filter.Open
|
||||||
|
}
|
||||||
|
if len(filter.Tags) > 0 {
|
||||||
|
query["tags"] = bson.M{"$all": filter.Tags}
|
||||||
|
}
|
||||||
|
if filter.Unlisted != nil {
|
||||||
|
query["listed"] = !*filter.Unlisted
|
||||||
|
} else {
|
||||||
|
query["listed"] = true
|
||||||
|
}
|
||||||
|
if !filter.EarliestFictionalDate.IsZero() && !filter.LatestFictionalDate.IsZero() {
|
||||||
|
query["fictionalDate"] = bson.M{
|
||||||
|
"$gte": filter.EarliestFictionalDate,
|
||||||
|
"$lte": filter.LatestFictionalDate,
|
||||||
|
}
|
||||||
|
} else if !filter.EarliestFictionalDate.IsZero() {
|
||||||
|
query["fictionalDate"] = bson.M{
|
||||||
|
"$gte": filter.EarliestFictionalDate,
|
||||||
|
}
|
||||||
|
} else if !filter.LatestFictionalDate.IsZero() {
|
||||||
|
query["fictionalDate"] = bson.M{
|
||||||
|
"$lte": filter.LatestFictionalDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stories := make([]*models.Story, 0, 32)
|
||||||
|
err := r.stories.Find(query).Sort("-updatedDate").Limit(filter.Limit).All(&stories)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return stories, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stories, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyRepository) Insert(ctx context.Context, story models.Story) (*models.Story, error) {
|
||||||
|
story.ID = generate.StoryID()
|
||||||
|
|
||||||
|
err := r.stories.Insert(story)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &story, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyRepository) Update(ctx context.Context, story models.Story, update models.StoryUpdate) (*models.Story, error) {
|
||||||
|
updateBson := bson.M{}
|
||||||
|
if update.Name != nil {
|
||||||
|
updateBson["name"] = *update.Name
|
||||||
|
story.Name = *update.Name
|
||||||
|
}
|
||||||
|
if update.Open != nil {
|
||||||
|
updateBson["open"] = *update.Open
|
||||||
|
story.Open = *update.Open
|
||||||
|
}
|
||||||
|
if update.Category != nil {
|
||||||
|
updateBson["category"] = *update.Category
|
||||||
|
story.Category = *update.Category
|
||||||
|
}
|
||||||
|
if update.Author != nil {
|
||||||
|
updateBson["author"] = *update.Author
|
||||||
|
story.Author = *update.Author
|
||||||
|
}
|
||||||
|
if update.FictionalDate != nil {
|
||||||
|
updateBson["fictionalDate"] = *update.FictionalDate
|
||||||
|
story.FictionalDate = *update.FictionalDate
|
||||||
|
}
|
||||||
|
if update.UpdatedDate != nil {
|
||||||
|
updateBson["updatedDate"] = *update.UpdatedDate
|
||||||
|
story.UpdatedDate = *update.UpdatedDate
|
||||||
|
}
|
||||||
|
if update.Listed != nil {
|
||||||
|
updateBson["listed"] = *update.Listed
|
||||||
|
story.Listed = *update.Listed
|
||||||
|
}
|
||||||
|
if update.SortByFictionalDate != nil {
|
||||||
|
updateBson["sortByFictionalDate"] = *update.SortByFictionalDate
|
||||||
|
story.SortByFictionalDate = *update.SortByFictionalDate
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.stories.UpdateId(story.ID, bson.M{"$set": updateBson})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &story, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyRepository) AddTag(ctx context.Context, story models.Story, tag models.Tag) error {
|
||||||
|
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$addToSet": bson.M{"tags": tag}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ci.Updated == 0 {
|
||||||
|
return repositories.ErrTagExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyRepository) RemoveTag(ctx context.Context, story models.Story, tag models.Tag) error {
|
||||||
|
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$pull": bson.M{"tags": tag}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ci.Updated == 0 {
|
||||||
|
return repositories.ErrTagDoesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyRepository) Delete(ctx context.Context, story models.Story) error {
|
||||||
|
err := r.stories.RemoveId(story.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
chapterIds := make([]string, 0, 8)
|
||||||
|
err = r.chapters.Find(bson.M{"storyId": story.ID}).Distinct("_id", &chapterIds)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to find chapterIds:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(chapterIds) > 0 {
|
||||||
|
c1, err := r.chapters.RemoveAll(bson.M{"storyId": story.ID})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to remove chapters:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c2, err := r.comments.RemoveAll(bson.M{"chapterId": bson.M{"$in": chapterIds}})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to remove comments:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Removed story %s (%d chapters, %d comments)", story.ID, c1.Removed, c2.Removed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tagRepository struct {
|
||||||
|
stories *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTagRepository(db *mgo.Database) repositories.TagRepository {
|
||||||
|
return &tagRepository{
|
||||||
|
stories: db.C("story.stories"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tagRepository) Find(ctx context.Context, kind models.TagKind, name string) (*models.Tag, error) {
|
||||||
|
tags := make([]*models.Tag, 0, 1)
|
||||||
|
err := r.stories.Find(bson.M{"listed": true, "tags": bson.M{"kind": kind, "name": name}}).Distinct("tag", &tags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(tags) == 0 {
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag.Kind == kind && tag.Name == name {
|
||||||
|
return tag, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tagRepository) List(ctx context.Context, filter models.TagFilter) ([]*models.Tag, error) {
|
||||||
|
tags := make([]*models.Tag, 0, 64)
|
||||||
|
|
||||||
|
query := bson.M{"listed": true, "tags": bson.M{"$ne": nil}}
|
||||||
|
if filter.Kind != nil {
|
||||||
|
query["tags.kind"] = *filter.Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.stories.Find(query).Distinct("tags", &tags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Kind != nil {
|
||||||
|
// While it's unsorted, delete any incorrect tags with replacement.
|
||||||
|
for i := 0; i < len(tags); i++ {
|
||||||
|
if tags[i].Kind != *filter.Kind {
|
||||||
|
tags[i] = tags[len(tags)-1]
|
||||||
|
tags = tags[:len(tags)-1]
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(tags, func(i, j int) bool {
|
||||||
|
if filter.Kind == nil {
|
||||||
|
kindCmp := strings.Compare(string(tags[i].Kind), string(tags[j].Kind))
|
||||||
|
if kindCmp != 0 {
|
||||||
|
return kindCmp < 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Compare(tags[i].Name, tags[j].Name) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userRepository struct {
|
||||||
|
users *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUserRepository(db *mgo.Database) (repositories.UserRepository, error) {
|
||||||
|
collection := db.C("core.users")
|
||||||
|
|
||||||
|
return &userRepository{users: collection}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) Find(ctx context.Context, id string) (*models.User, error) {
|
||||||
|
user := new(models.User)
|
||||||
|
err := r.users.FindId(id).One(user)
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return nil, repositories.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) Insert(ctx context.Context, user models.User) (*models.User, error) {
|
||||||
|
err := r.users.Insert(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
module git.aiterp.net/rpdata/api
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/99designs/gqlgen v0.9.0
|
||||||
|
github.com/beorn7/perks v1.0.0 // indirect
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
|
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e
|
||||||
|
github.com/go-ini/ini v1.35.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.3.1 // indirect
|
||||||
|
github.com/google/go-cmp v0.3.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.0 // indirect
|
||||||
|
github.com/h2non/filetype v1.0.8
|
||||||
|
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05
|
||||||
|
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 // indirect
|
||||||
|
github.com/prometheus/client_golang v0.9.1
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
|
||||||
|
github.com/prometheus/common v0.4.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e // indirect
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
|
||||||
|
github.com/stretchr/testify v1.3.0
|
||||||
|
github.com/tidwall/pretty v1.0.0 // indirect
|
||||||
|
github.com/vektah/dataloaden v0.3.0
|
||||||
|
github.com/vektah/gqlparser v1.1.2
|
||||||
|
go.mongodb.org/mongo-driver v1.0.3
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20190514140710-3ec191127204 // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||||
|
golang.org/x/text v0.3.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.42.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.2
|
||||||
|
)
|
||||||
|
|
||||||
|
replace golang.org/x/net => golang.org/x/net v0.0.0-20180416171110-a35a21de978d
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
github.com/99designs/gqlgen v0.9.0 h1:g1arBPML74Vqv0L3Q+TqIhGXLspV+2MYtRLkBxuZrlE=
|
||||||
|
github.com/99designs/gqlgen v0.9.0/go.mod h1:HrrG7ic9EgLPsULxsZh/Ti+p0HNWgR3XRuvnD0pb5KY=
|
||||||
|
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
|
||||||
|
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e h1:hmrbHva/wKD6X4M7pgHVbg/KfVV1wwgr1NEkQaVD3rU=
|
||||||
|
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||||
|
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||||
|
github.com/go-ini/ini v1.35.0 h1:D/my3+xOfqZMkJpciRcyqU7XMBUgiZa9qXjZIa8uv2k=
|
||||||
|
github.com/go-ini/ini v1.35.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/h2non/filetype v1.0.8 h1:le8gpf+FQA0/DlDABbtisA1KiTS0Xi+YSC/E8yY3Y14=
|
||||||
|
github.com/h2non/filetype v1.0.8/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05 h1:YEyFCqmHcmbcTLqZ7yEAg7VIUat6PuDzGbIctZy888k=
|
||||||
|
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
|
||||||
|
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk=
|
||||||
|
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||||
|
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
|
||||||
|
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e h1:zK8d1aZ+gw/Ne4uMfZTFRxj08PUOp+gGwm4HWUeGI1k=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||||
|
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||||
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||||
|
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
|
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||||
|
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
|
||||||
|
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||||
|
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
|
||||||
|
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||||
|
go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU=
|
||||||
|
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20180416171110-a35a21de978d h1:O2P57H5Cc+d+DJos+iweraI9rmzMYwV+45vkZdDY0Oo=
|
||||||
|
golang.org/x/net v0.0.0-20180416171110-a35a21de978d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5 h1:mzjBh+S5frKOsOBobWIMAbXavqjmgO17k/2puhcFR94=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6 h1:iZgcI2DDp6zW5v9Z/5+f0NuqoxNdmzg4hivjk2WLXpY=
|
||||||
|
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
|
||||||
|
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||||
|
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||||
|
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/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,186 @@
|
|||||||
|
package graph2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func complexity() (cr graphcore.ComplexityRoot) {
|
||||||
|
subListComplexity := 25
|
||||||
|
bigSublistComplexity := 75
|
||||||
|
hugeSublistComplexity := 100
|
||||||
|
findComplexity := 100
|
||||||
|
listComplexity := 200
|
||||||
|
mutationComplexity := 100
|
||||||
|
|
||||||
|
cr.Query.Character = func(childComplexity int, id *string, nick *string) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Characters = func(childComplexity int, filter *models.CharacterFilter) int {
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Channel = func(childComplexity int, name string) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Channels = func(childComplexity int, filter *models.ChannelFilter) int {
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Post = func(childComplexity int, id string) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Posts = func(childComplexity int, filter *models.PostFilter) int {
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.UnknownNicks = func(childComplexity int, filter *graphcore.UnknownNicksFilter) int {
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Log = func(childComplexity int, id string) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Logs = func(childComplexity int, filter *models.LogFilter) int {
|
||||||
|
if filter != nil && ((filter.Open != nil && *filter.Open == true) || (filter.Limit <= 10)) {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Chapter = func(childComplexity int, id string) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Comment = func(childComplexity int, id string) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Tags = func(childComplexity int, filter *graphcore.TagFilter) int {
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Story = func(childComplexity int, id string) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Stories = func(childComplexity int, filter *models.StoryFilter) int {
|
||||||
|
if filter != nil && filter.Limit <= 10 {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.File = func(childComplexity int, id string) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Files = func(childComplexity int, filter *models.FileFilter) int {
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Changes = func(childComplexity int, filter *models.ChangeFilter) int {
|
||||||
|
return childComplexity + listComplexity
|
||||||
|
}
|
||||||
|
cr.Query.Token = func(childComplexity int) int {
|
||||||
|
return childComplexity + findComplexity
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.Mutation.AddStory = func(childComplexity int, input graphcore.StoryAddInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.AddStoryTag = func(childComplexity int, input graphcore.StoryTagAddInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.RemoveStoryTag = func(childComplexity int, input graphcore.StoryTagRemoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.EditStory = func(childComplexity int, input graphcore.StoryEditInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.RemoveStory = func(childComplexity int, input graphcore.StoryRemoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.AddChapter = func(childComplexity int, input graphcore.ChapterAddInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.EditChapter = func(childComplexity int, input graphcore.ChapterEditInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.MoveChapter = func(childComplexity int, input graphcore.ChapterMoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.RemoveChapter = func(childComplexity int, input graphcore.ChapterRemoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.AddComment = func(childComplexity int, input graphcore.CommentAddInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.EditComment = func(childComplexity int, input graphcore.CommentEditInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.RemoveComment = func(childComplexity int, input graphcore.CommentRemoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.AddLog = func(childComplexity int, input graphcore.LogAddInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.ImportLog = func(childComplexity int, input graphcore.LogImportInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.EditLog = func(childComplexity int, input graphcore.LogEditInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.RemoveLog = func(childComplexity int, input graphcore.LogRemoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.AddPost = func(childComplexity int, input graphcore.PostAddInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.EditPost = func(childComplexity int, input graphcore.PostEditInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.MovePost = func(childComplexity int, input graphcore.PostMoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.RemovePost = func(childComplexity int, input graphcore.PostRemoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.AddCharacter = func(childComplexity int, input graphcore.CharacterAddInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.AddCharacterNick = func(childComplexity int, input graphcore.CharacterNickInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.RemoveCharacterNick = func(childComplexity int, input graphcore.CharacterNickInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.EditCharacter = func(childComplexity int, input graphcore.CharacterEditInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.RemoveCharacter = func(childComplexity int, input graphcore.CharacterRemoveInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.AddChannel = func(childComplexity int, input graphcore.ChannelAddInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
cr.Mutation.EditChannel = func(childComplexity int, input graphcore.ChannelEditInput) int {
|
||||||
|
return childComplexity + mutationComplexity
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.Log.Characters = func(childComplexity int) int {
|
||||||
|
return childComplexity + subListComplexity
|
||||||
|
}
|
||||||
|
cr.Log.Posts = func(childComplexity int, kinds []string) int {
|
||||||
|
return childComplexity + hugeSublistComplexity
|
||||||
|
}
|
||||||
|
cr.Log.NextLogs = func(childComplexity int) int {
|
||||||
|
return childComplexity + (subListComplexity / 2)
|
||||||
|
}
|
||||||
|
cr.Log.PrevLogs = func(childComplexity int) int {
|
||||||
|
return childComplexity + (subListComplexity / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.Story.Chapters = func(childComplexity int) int {
|
||||||
|
return childComplexity + bigSublistComplexity
|
||||||
|
}
|
||||||
|
cr.Story.Tags = func(childComplexity int) int {
|
||||||
|
return childComplexity + subListComplexity
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.Chapter.Comments = func(childComplexity int, limit *int) int {
|
||||||
|
return childComplexity + subListComplexity
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
+24
-20
@@ -1,12 +1,14 @@
|
|||||||
schema: generated.gql
|
schema:
|
||||||
|
- schema/root.gql
|
||||||
|
- schema/types/*.gql
|
||||||
|
|
||||||
exec:
|
exec:
|
||||||
filename: generated.go
|
filename: graphcore/exec_gen.go
|
||||||
package: graph2
|
package: graphcore
|
||||||
|
|
||||||
model:
|
model:
|
||||||
filename: input/generated.go
|
filename: graphcore/input_gen.go
|
||||||
package: input
|
package: graphcore
|
||||||
|
|
||||||
models:
|
models:
|
||||||
Tag:
|
Tag:
|
||||||
@@ -18,37 +20,41 @@ models:
|
|||||||
Character:
|
Character:
|
||||||
model: git.aiterp.net/rpdata/api/models.Character
|
model: git.aiterp.net/rpdata/api/models.Character
|
||||||
CharactersFilter:
|
CharactersFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/characters.Filter
|
model: git.aiterp.net/rpdata/api/models.CharacterFilter
|
||||||
Channel:
|
Channel:
|
||||||
model: git.aiterp.net/rpdata/api/models.Channel
|
model: git.aiterp.net/rpdata/api/models.Channel
|
||||||
ChannelsFilter:
|
ChannelsFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/channels.Filter
|
model: git.aiterp.net/rpdata/api/models.ChannelFilter
|
||||||
|
UnknownNick:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.UnknownNick
|
||||||
Post:
|
Post:
|
||||||
model: git.aiterp.net/rpdata/api/models.Post
|
model: git.aiterp.net/rpdata/api/models.Post
|
||||||
PostsFilter:
|
PostsFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/posts.Filter
|
model: git.aiterp.net/rpdata/api/models.PostFilter
|
||||||
Log:
|
Log:
|
||||||
model: git.aiterp.net/rpdata/api/models.Log
|
model: git.aiterp.net/rpdata/api/models.Log
|
||||||
LogsFilter:
|
LogsFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/logs.Filter
|
model: git.aiterp.net/rpdata/api/models.LogFilter
|
||||||
|
LogImporter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.LogImporter
|
||||||
|
LogSuggestion:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.LogSuggestion
|
||||||
|
Comment:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Comment
|
||||||
|
ChapterCommentMode:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChapterCommentMode
|
||||||
Chapter:
|
Chapter:
|
||||||
model: git.aiterp.net/rpdata/api/models.Chapter
|
model: git.aiterp.net/rpdata/api/models.Chapter
|
||||||
fields:
|
|
||||||
fictionalDate:
|
|
||||||
resolver: true
|
|
||||||
Story:
|
Story:
|
||||||
model: git.aiterp.net/rpdata/api/models.Story
|
model: git.aiterp.net/rpdata/api/models.Story
|
||||||
fields:
|
|
||||||
fictionalDate:
|
|
||||||
resolver: true
|
|
||||||
StoryCategory:
|
StoryCategory:
|
||||||
model: git.aiterp.net/rpdata/api/models.StoryCategory
|
model: git.aiterp.net/rpdata/api/models.StoryCategory
|
||||||
StoriesFilter:
|
StoriesFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/stories.Filter
|
model: git.aiterp.net/rpdata/api/models.StoryFilter
|
||||||
File:
|
File:
|
||||||
model: git.aiterp.net/rpdata/api/models.File
|
model: git.aiterp.net/rpdata/api/models.File
|
||||||
FilesFilter:
|
FilesFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/files.Filter
|
model: git.aiterp.net/rpdata/api/models.FileFilter
|
||||||
Change:
|
Change:
|
||||||
model: git.aiterp.net/rpdata/api/models.Change
|
model: git.aiterp.net/rpdata/api/models.Change
|
||||||
ChangeModel:
|
ChangeModel:
|
||||||
@@ -58,10 +64,8 @@ models:
|
|||||||
ChangeKeyInput: # It's the same as ChangeKey
|
ChangeKeyInput: # It's the same as ChangeKey
|
||||||
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
||||||
ChangesFilter:
|
ChangesFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/changes.Filter
|
model: git.aiterp.net/rpdata/api/models.ChangeFilter
|
||||||
Token:
|
Token:
|
||||||
model: git.aiterp.net/rpdata/api/models.Token
|
model: git.aiterp.net/rpdata/api/models.Token
|
||||||
User:
|
User:
|
||||||
model: git.aiterp.net/rpdata/api/models.User
|
model: git.aiterp.net/rpdata/api/models.User
|
||||||
Date:
|
|
||||||
model: git.aiterp.net/rpdata/api/models/scalars.Date
|
|
||||||
+31
-23
@@ -1,51 +1,59 @@
|
|||||||
package graph2
|
package graph2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.aiterp.net/rpdata/api/graph2/queries"
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/resolvers"
|
||||||
"git.aiterp.net/rpdata/api/graph2/types"
|
"git.aiterp.net/rpdata/api/graph2/types"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
graphql "github.com/99designs/gqlgen/graphql"
|
graphql "github.com/99designs/gqlgen/graphql"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate ./combine.sh
|
//go:generate go run github.com/99designs/gqlgen -v
|
||||||
//go:generate gorunpkg github.com/99designs/gqlgen -v
|
|
||||||
|
|
||||||
// New creates a new GraphQL schema.
|
// New creates a new GraphQL schema.
|
||||||
func New() graphql.ExecutableSchema {
|
func New(s *services.Bundle) graphql.ExecutableSchema {
|
||||||
return NewExecutableSchema(Config{
|
return graphcore.NewExecutableSchema(graphcore.Config{
|
||||||
Resolvers: &rootResolver{},
|
Resolvers: &rootResolver{s},
|
||||||
|
Complexity: complexity(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type rootResolver struct{}
|
type rootResolver struct {
|
||||||
|
s *services.Bundle
|
||||||
func (r *rootResolver) Query() QueryResolver {
|
|
||||||
return &queries.Resolver
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rootResolver) Mutation() MutationResolver {
|
func (r *rootResolver) Query() graphcore.QueryResolver {
|
||||||
return queries.MutationResolver
|
return resolvers.QueryResolver(r.s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rootResolver) Log() LogResolver {
|
func (r *rootResolver) Mutation() graphcore.MutationResolver {
|
||||||
return &types.LogResolver
|
return resolvers.MutationResolver(r.s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rootResolver) Chapter() ChapterResolver {
|
func (r *rootResolver) Subscription() graphcore.SubscriptionResolver {
|
||||||
return &types.ChapterResolver
|
return resolvers.SubscriptionResolver(r.s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rootResolver) Story() StoryResolver {
|
func (r *rootResolver) Log() graphcore.LogResolver {
|
||||||
return &types.StoryResolver
|
return types.LogResolver(r.s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rootResolver) File() FileResolver {
|
func (r *rootResolver) Comment() graphcore.CommentResolver {
|
||||||
return &types.FileResolver
|
return types.CommentResolver(r.s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rootResolver) Change() ChangeResolver {
|
func (r *rootResolver) Chapter() graphcore.ChapterResolver {
|
||||||
|
return types.ChapterResolver(r.s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Story() graphcore.StoryResolver {
|
||||||
|
return types.StoryResolver(r.s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Change() graphcore.ChangeResolver {
|
||||||
return &types.ChangeResolver
|
return &types.ChangeResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rootResolver) Token() TokenResolver {
|
func (r *rootResolver) Token() graphcore.TokenResolver {
|
||||||
return &types.TokenResolver
|
return types.TokenResolver(r.s)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package queries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
|
||||||
"git.aiterp.net/rpdata/api/models/changes"
|
|
||||||
)
|
|
||||||
|
|
||||||
/// Queries
|
|
||||||
|
|
||||||
func (r *resolver) Changes(ctx context.Context, filter *changes.Filter) ([]models.Change, error) {
|
|
||||||
return changes.List(filter)
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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/changekeys"
|
|
||||||
"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, true, changekeys.Listed(channel), 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, true, changekeys.Listed(channel), channel)
|
|
||||||
|
|
||||||
return channel, nil
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
package queries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
|
||||||
"git.aiterp.net/rpdata/api/models/changes"
|
|
||||||
|
|
||||||
"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")
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter, err := chapters.Add(story, input.Title, author, input.Source, time.Now(), input.FictionalDate)
|
|
||||||
if err != nil {
|
|
||||||
return models.Chapter{}, errors.New("Failed to create chapter: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go changes.Submit("Chapter", "add", token.UserID, story.Listed, changekeys.Listed(story, chapter), story, chapter)
|
|
||||||
|
|
||||||
return chapter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *mutationResolver) MoveChapter(ctx context.Context, input input.ChapterMoveInput) (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.move") {
|
|
||||||
return models.Chapter{}, errors.New("You are not allowed to move this chapter")
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := stories.FindID(input.StoryID)
|
|
||||||
if err != nil {
|
|
||||||
return models.Chapter{}, errors.New("Target story not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !target.Open && !token.PermittedUser(target.Author, "member", "chapter.move") {
|
|
||||||
return models.Chapter{}, errors.New("You are not permitted to move chapters to this story")
|
|
||||||
}
|
|
||||||
|
|
||||||
oldStoryID := chapter.StoryID
|
|
||||||
chapter, err = chapters.Move(chapter, target)
|
|
||||||
if err != nil {
|
|
||||||
return models.Chapter{}, errors.New("Failed to move chapter: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
story, err := stories.FindID(chapter.StoryID)
|
|
||||||
if err != nil {
|
|
||||||
story.ID = chapter.StoryID
|
|
||||||
}
|
|
||||||
|
|
||||||
oldStory, err := stories.FindID(oldStoryID)
|
|
||||||
if err != nil {
|
|
||||||
oldStory.ID = oldStoryID
|
|
||||||
}
|
|
||||||
|
|
||||||
changes.Submit("Chapter", "move-out", chapter.Author, oldStory.Listed, changekeys.Many(oldStory, chapter), chapter)
|
|
||||||
changes.Submit("Chapter", "move-in", token.UserID, story.Listed, changekeys.Listed(story, chapter), story, chapter)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return chapter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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{}
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter, err = chapters.Edit(chapter, input.Title, input.Source, input.FictionalDate)
|
|
||||||
if err != nil {
|
|
||||||
return models.Chapter{}, errors.New("Failed to edit chapter: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
story, err := stories.FindID(chapter.StoryID)
|
|
||||||
if err != nil {
|
|
||||||
story.ID = chapter.StoryID
|
|
||||||
}
|
|
||||||
|
|
||||||
changes.Submit("Chapter", "edit", token.UserID, story.Listed, changekeys.Many(story, chapter), chapter)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return chapter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter, err = chapters.Remove(chapter)
|
|
||||||
if err != nil {
|
|
||||||
return models.Chapter{}, errors.New("Failed to remove chapter: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
story, err := stories.FindID(chapter.StoryID)
|
|
||||||
if err != nil {
|
|
||||||
story.ID = chapter.StoryID
|
|
||||||
}
|
|
||||||
|
|
||||||
changes.Submit("Chapter", "remove", token.UserID, story.Listed, changekeys.Many(story, chapter), chapter)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return chapter, nil
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
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/changekeys"
|
|
||||||
"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, true, changekeys.Listed(character), 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, true, changekeys.Listed(character), 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, true, changekeys.Listed(character), 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, true, changekeys.Listed(character), 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, true, changekeys.Listed(character), character)
|
|
||||||
|
|
||||||
return character, nil
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
package queries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/channels"
|
|
||||||
|
|
||||||
"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/changekeys"
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
existingChannel, _ := channels.FindName(input.Channel)
|
|
||||||
|
|
||||||
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 func() {
|
|
||||||
if existingChannel.Name == "" {
|
|
||||||
channel, err := channels.FindName(input.Channel)
|
|
||||||
if err == nil {
|
|
||||||
changes.Submit("Channel", "add", token.UserID, true, changekeys.Listed(channel), channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changes.Submit("Log", "add", token.UserID, true, changekeys.Listed(log), 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, true, changekeys.Listed(log), 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, true, changekeys.Listed(log), log)
|
|
||||||
|
|
||||||
return log, nil
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
package queries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
|
||||||
"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, true, changekeys.Many(log, post), 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 func() {
|
|
||||||
log, err := logs.FindID(post.LogID)
|
|
||||||
if err != nil {
|
|
||||||
log = models.Log{ID: post.LogID}
|
|
||||||
}
|
|
||||||
|
|
||||||
changes.Submit("Post", "edit", token.UserID, true, changekeys.Many(log, post), 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())
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log, err := logs.FindID(post.LogID)
|
|
||||||
if err != nil {
|
|
||||||
log = models.Log{ID: post.LogID}
|
|
||||||
}
|
|
||||||
|
|
||||||
changes.Submit("Post", "move", token.UserID, true, changekeys.Many(log, posts), posts)
|
|
||||||
}()
|
|
||||||
|
|
||||||
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)
|
|
||||||
changes.Submit("Post", "remove", token.UserID, true, changekeys.Many(log, post), post)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return post, nil
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package queries
|
|
||||||
|
|
||||||
type resolver struct{}
|
|
||||||
type mutationResolver struct{}
|
|
||||||
|
|
||||||
// Resolver has all the queries
|
|
||||||
var Resolver resolver
|
|
||||||
|
|
||||||
// MutationResolver brings the mutagens.
|
|
||||||
var MutationResolver *mutationResolver
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
package queries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
|
||||||
"git.aiterp.net/rpdata/api/models/changes"
|
|
||||||
|
|
||||||
"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
|
|
||||||
|
|
||||||
story, err := stories.Add(input.Name, author, input.Category, listed, open, input.Tags, time.Now(), fictionalDate)
|
|
||||||
if err != nil {
|
|
||||||
return models.Story{}, errors.New("Failed to add story: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go changes.Submit("Story", "add", token.UserID, story.Listed, changekeys.Listed(story), story)
|
|
||||||
|
|
||||||
return story, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
story, err = stories.AddTag(story, input.Tag)
|
|
||||||
if err != nil {
|
|
||||||
return models.Story{}, errors.New("Failed to add story: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go changes.Submit("Story", "tag", token.UserID, story.Listed, changekeys.Listed(story), story, input.Tag)
|
|
||||||
|
|
||||||
return story, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
story, err = stories.RemoveTag(story, input.Tag)
|
|
||||||
if err != nil {
|
|
||||||
return models.Story{}, errors.New("Failed to add story: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go changes.Submit("Story", "untag", token.UserID, story.Listed, changekeys.Listed(story), story, input.Tag)
|
|
||||||
|
|
||||||
return story, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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{}
|
|
||||||
}
|
|
||||||
|
|
||||||
story, err = stories.Edit(story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate)
|
|
||||||
if err != nil {
|
|
||||||
return models.Story{}, errors.New("Failed to add story: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go changes.Submit("Story", "edit", token.UserID, story.Listed, changekeys.Listed(story), story)
|
|
||||||
|
|
||||||
return story, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
go changes.Submit("Story", "remove", token.UserID, story.Listed, changekeys.Listed(story), story)
|
|
||||||
|
|
||||||
return story, nil
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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,26 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
/// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) Changes(ctx context.Context, filter *models.ChangeFilter) ([]*models.Change, error) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &models.ChangeFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Changes.List(ctx, *filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscriptions
|
||||||
|
|
||||||
|
func (r *subscriptionResolver) Changes(ctx context.Context, filter *models.ChangeFilter) (<-chan *models.Change, error) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &models.ChangeFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Changes.Subscribe(ctx, *filter), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) Channel(ctx context.Context, name string) (*models.Channel, error) {
|
||||||
|
return r.s.Channels.Find(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Channels(ctx context.Context, filter *models.ChannelFilter) ([]*models.Channel, error) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &models.ChannelFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Channels.List(ctx, *filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddChannel(ctx context.Context, input graphcore.ChannelAddInput) (*models.Channel, error) {
|
||||||
|
logged := input.Logged != nil && *input.Logged
|
||||||
|
hub := input.Hub != nil && *input.Hub
|
||||||
|
|
||||||
|
locationName := ""
|
||||||
|
if input.LocationName != nil {
|
||||||
|
locationName = *input.LocationName
|
||||||
|
}
|
||||||
|
|
||||||
|
eventName := ""
|
||||||
|
if input.LocationName != nil {
|
||||||
|
eventName = *input.EventName
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Channels.Create(ctx, input.Name, logged, hub, eventName, locationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditChannel(ctx context.Context, input graphcore.ChannelEditInput) (*models.Channel, error) {
|
||||||
|
return r.s.Channels.Update(ctx, input.Name, models.ChannelUpdate{
|
||||||
|
Logged: input.Logged,
|
||||||
|
Hub: input.Hub,
|
||||||
|
EventName: input.EventName,
|
||||||
|
LocationName: input.LocationName,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) Chapter(ctx context.Context, id string) (*models.Chapter, error) {
|
||||||
|
return r.s.Stories.FindChapter(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddChapter(ctx context.Context, input graphcore.ChapterAddInput) (*models.Chapter, error) {
|
||||||
|
story, err := r.s.Stories.FindStory(ctx, input.StoryID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commentMode := models.ChapterCommentModeDisabled
|
||||||
|
if input.CommentMode != nil {
|
||||||
|
commentMode = *input.CommentMode
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.CreateChapter(ctx, *story, input.Title, input.Source, input.Author, time.Now(), input.FictionalDate, commentMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) MoveChapter(ctx context.Context, input graphcore.ChapterMoveInput) (*models.Chapter, error) {
|
||||||
|
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
from, err := r.s.Stories.FindStory(ctx, chapter.StoryID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
to, err := r.s.Stories.FindStory(ctx, input.StoryID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.MoveChapter(ctx, chapter, *from, *to)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditChapter(ctx context.Context, input graphcore.ChapterEditInput) (*models.Chapter, error) {
|
||||||
|
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.EditChapter(ctx, chapter, input.Title, input.Source, input.FictionalDate, input.CommentMode, input.CommentsLocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemoveChapter(ctx context.Context, input graphcore.ChapterRemoveInput) (*models.Chapter, error) {
|
||||||
|
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.s.Stories.RemoveChapter(ctx, chapter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapter, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) Character(ctx context.Context, id *string, nick *string) (*models.Character, error) {
|
||||||
|
if id != nil {
|
||||||
|
return r.s.Characters.Find(ctx, *id)
|
||||||
|
} else if nick != nil {
|
||||||
|
return r.s.Characters.FindNick(ctx, *nick)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("You must specify either an ID or a nick")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Characters(ctx context.Context, filter *models.CharacterFilter) ([]*models.Character, error) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &models.CharacterFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Characters.List(ctx, *filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddCharacter(ctx context.Context, input graphcore.CharacterAddInput) (*models.Character, error) {
|
||||||
|
author := ""
|
||||||
|
if input.Author != nil {
|
||||||
|
author = *input.Author
|
||||||
|
}
|
||||||
|
|
||||||
|
shortName := ""
|
||||||
|
if input.ShortName != nil {
|
||||||
|
shortName = *input.ShortName
|
||||||
|
}
|
||||||
|
|
||||||
|
description := ""
|
||||||
|
if input.Description != nil {
|
||||||
|
description = *input.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Characters.Create(ctx, input.Nick, input.Name, shortName, author, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddCharacterNick(ctx context.Context, input graphcore.CharacterNickInput) (*models.Character, error) {
|
||||||
|
return r.s.Characters.AddNick(ctx, input.ID, input.Nick)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemoveCharacterNick(ctx context.Context, input graphcore.CharacterNickInput) (*models.Character, error) {
|
||||||
|
return r.s.Characters.RemoveNick(ctx, input.ID, input.Nick)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditCharacter(ctx context.Context, input graphcore.CharacterEditInput) (*models.Character, error) {
|
||||||
|
return r.s.Characters.Update(ctx, input.ID, input.Name, input.ShortName, input.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemoveCharacter(ctx context.Context, input graphcore.CharacterRemoveInput) (*models.Character, error) {
|
||||||
|
return r.s.Characters.Delete(ctx, input.ID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) Comment(ctx context.Context, id string) (*models.Comment, error) {
|
||||||
|
return r.s.Stories.FindComment(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddComment(ctx context.Context, input graphcore.CommentAddInput) (*models.Comment, error) {
|
||||||
|
chapter, err := r.s.Stories.FindChapter(ctx, input.ChapterID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := ""
|
||||||
|
if input.Subject != nil {
|
||||||
|
subject = *input.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
fictionalDate := time.Time{}
|
||||||
|
if input.FictionalDate != nil {
|
||||||
|
fictionalDate = *input.FictionalDate
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.CreateComment(ctx, *chapter, subject, "", input.Source, input.CharacterName, input.CharacterID, time.Now(), fictionalDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditComment(ctx context.Context, input graphcore.CommentEditInput) (*models.Comment, error) {
|
||||||
|
comment, err := r.s.Stories.FindComment(ctx, input.CommentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fictionalDate := input.FictionalDate
|
||||||
|
if input.ClearFictionalDate != nil && *input.ClearFictionalDate {
|
||||||
|
fictionalDate = &time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.EditComment(ctx, comment, input.Source, input.CharacterName, input.CharacterID, input.Subject, fictionalDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemoveComment(ctx context.Context, input graphcore.CommentRemoveInput) (*models.Comment, error) {
|
||||||
|
comment, err := r.s.Stories.FindComment(ctx, input.CommentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.s.Stories.RemoveComment(ctx, comment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) File(ctx context.Context, id string) (*models.File, error) {
|
||||||
|
return r.s.Files.Find(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Files(ctx context.Context, filter *models.FileFilter) ([]*models.File, error) {
|
||||||
|
if filter == nil {
|
||||||
|
public := true
|
||||||
|
|
||||||
|
filter = &models.FileFilter{
|
||||||
|
Public: &public,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Files.List(ctx, *filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
|
||||||
|
func (r *mutationResolver) UploadFile(ctx context.Context, input *graphcore.FileUploadInput) (*models.File, error) {
|
||||||
|
return r.s.Files.Upload(ctx, input.File.File, input.Name, input.Public, input.File.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditFile(ctx context.Context, input *graphcore.FileEditInput) (*models.File, error) {
|
||||||
|
return r.s.Files.Edit(ctx, input.ID, input.Name, input.Public)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemoveFile(ctx context.Context, input *graphcore.FileRemoveInput) (*models.File, error) {
|
||||||
|
return r.s.Files.Remove(ctx, input.ID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) Log(ctx context.Context, id string) (*models.Log, error) {
|
||||||
|
return r.s.Logs.Find(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Logs(ctx context.Context, filter *models.LogFilter) ([]*models.Log, error) {
|
||||||
|
return r.s.Logs.List(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddLog(ctx context.Context, input graphcore.LogAddInput) (*models.Log, error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Logs.Create(ctx, title, description, input.Channel, event, open)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) ImportLog(ctx context.Context, input graphcore.LogImportInput) ([]*models.Log, error) {
|
||||||
|
date := time.Time{}
|
||||||
|
if input.Date != nil {
|
||||||
|
date = *input.Date
|
||||||
|
}
|
||||||
|
|
||||||
|
tz := time.UTC
|
||||||
|
if input.Timezone != nil {
|
||||||
|
parsedTZ, err := time.LoadLocation(*input.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Unknown timezone: " + *input.Timezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
tz = parsedTZ
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionThreshold := time.Hour * 12
|
||||||
|
if input.SessionThresholdMs != nil {
|
||||||
|
sessionThreshold = time.Duration(*input.SessionThresholdMs) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Logs.Import(ctx, input.Importer, date, tz, input.ChannelName, sessionThreshold, input.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) SplitLog(ctx context.Context, input graphcore.LogSplitInput) (*models.Log, error) {
|
||||||
|
return r.s.Logs.SplitLog(ctx, input.LogID, input.StartPostID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) MergeLog(ctx context.Context, input graphcore.LogMergeInput) (*models.Log, error) {
|
||||||
|
removeAfter := input.RemoveAfter != nil && *input.RemoveAfter
|
||||||
|
|
||||||
|
return r.s.Logs.MergeLogs(ctx, input.TargetLogID, input.SourceLogID, removeAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditLog(ctx context.Context, input graphcore.LogEditInput) (*models.Log, error) {
|
||||||
|
update := models.LogUpdate{
|
||||||
|
Open: input.Open,
|
||||||
|
Description: input.Description,
|
||||||
|
EventName: input.Event,
|
||||||
|
Title: input.Title,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Logs.Update(ctx, input.ID, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemoveLog(ctx context.Context, input graphcore.LogRemoveInput) (*models.Log, error) {
|
||||||
|
return r.s.Logs.Delete(ctx, input.ID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) Post(ctx context.Context, id string) (*models.Post, error) {
|
||||||
|
return r.s.Logs.FindPosts(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Posts(ctx context.Context, filter *models.PostFilter) ([]*models.Post, error) {
|
||||||
|
return r.s.Logs.ListPosts(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutation
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddPost(ctx context.Context, input graphcore.PostAddInput) (*models.Post, error) {
|
||||||
|
return r.s.Logs.AddPost(ctx, input.LogID, input.Time, input.Kind, input.Nick, input.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditPost(ctx context.Context, input graphcore.PostEditInput) (*models.Post, error) {
|
||||||
|
return r.s.Logs.EditPost(ctx, input.ID, models.PostUpdate{
|
||||||
|
Time: input.Time,
|
||||||
|
Kind: input.Kind,
|
||||||
|
Nick: input.Nick,
|
||||||
|
Text: input.Text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) MovePost(ctx context.Context, input graphcore.PostMoveInput) ([]*models.Post, error) {
|
||||||
|
return r.s.Logs.MovePost(ctx, input.ID, input.ToPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemovePost(ctx context.Context, input graphcore.PostRemoveInput) (*models.Post, error) {
|
||||||
|
return r.s.Logs.DeletePost(ctx, input.ID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type queryResolver struct{ s *services.Bundle }
|
||||||
|
type mutationResolver struct{ s *services.Bundle }
|
||||||
|
|
||||||
|
type subscriptionResolver struct{ s *services.Bundle }
|
||||||
|
|
||||||
|
// QueryResolver has all the queries
|
||||||
|
func QueryResolver(s *services.Bundle) graphcore.QueryResolver {
|
||||||
|
return &queryResolver{s: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MutationResolver brings the radioactive goop
|
||||||
|
func MutationResolver(s *services.Bundle) graphcore.MutationResolver {
|
||||||
|
return &mutationResolver{s: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionResolver has the real-time magic.
|
||||||
|
func SubscriptionResolver(s *services.Bundle) graphcore.SubscriptionResolver {
|
||||||
|
return &subscriptionResolver{s: s}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *queryResolver) Story(ctx context.Context, id string) (*models.Story, error) {
|
||||||
|
return r.s.Stories.FindStory(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Stories(ctx context.Context, filter *models.StoryFilter) ([]*models.Story, error) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &models.StoryFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.ListStories(ctx, *filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddStory(ctx context.Context, input graphcore.StoryAddInput) (*models.Story, error) {
|
||||||
|
listed := input.Listed != nil && *input.Listed
|
||||||
|
open := input.Open != nil && *input.Open
|
||||||
|
|
||||||
|
tags := make([]models.Tag, len(input.Tags))
|
||||||
|
for i, tag := range input.Tags {
|
||||||
|
tags[i] = *tag
|
||||||
|
}
|
||||||
|
|
||||||
|
fictionalDate := time.Time{}
|
||||||
|
if input.FictionalDate != nil {
|
||||||
|
fictionalDate = *input.FictionalDate
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByFictionalDate := input.SortByFictionalDate != nil && *input.SortByFictionalDate
|
||||||
|
|
||||||
|
return r.s.Stories.CreateStory(ctx, input.Name, input.Author, input.Category, listed, open, tags, time.Now(), fictionalDate, sortByFictionalDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) AddStoryTag(ctx context.Context, input graphcore.StoryTagAddInput) (*models.Story, error) {
|
||||||
|
story, err := r.s.Stories.FindStory(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.AddStoryTag(ctx, *story, *input.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemoveStoryTag(ctx context.Context, input graphcore.StoryTagRemoveInput) (*models.Story, error) {
|
||||||
|
story, err := r.s.Stories.FindStory(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.RemoveStoryTag(ctx, *story, *input.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditStory(ctx context.Context, input graphcore.StoryEditInput) (*models.Story, error) {
|
||||||
|
story, err := r.s.Stories.FindStory(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Stories.EditStory(ctx, story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate, input.SortByFictionalDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) RemoveStory(ctx context.Context, input graphcore.StoryRemoveInput) (*models.Story, error) {
|
||||||
|
story, err := r.s.Stories.FindStory(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.s.Stories.RemoveStory(ctx, story)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return story, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *queryResolver) Tags(ctx context.Context, filter *graphcore.TagFilter) ([]*models.Tag, error) {
|
||||||
|
tagFilter := models.TagFilter{}
|
||||||
|
if filter != nil && filter.Kind != nil {
|
||||||
|
tagFilter.Kind = filter.Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.s.Tags.ListTags(ctx, tagFilter)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *queryResolver) Token(ctx context.Context) (*models.Token, error) {
|
||||||
|
token := r.s.Auth.TokenFromContext(ctx)
|
||||||
|
if !token.Authenticated() {
|
||||||
|
return nil, errors.New("no (valid) token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
/// Queries
|
||||||
|
|
||||||
|
func (r *queryResolver) UnknownNicks(ctx context.Context, filter *graphcore.UnknownNicksFilter) ([]*models.UnknownNick, error) {
|
||||||
|
limit := 20
|
||||||
|
if filter != nil {
|
||||||
|
limit = filter.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
nicks := r.s.Logs.ListUnknownNicks()
|
||||||
|
if limit > 0 && len(nicks) > limit {
|
||||||
|
nicks = nicks[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nicks, nil
|
||||||
|
}
|
||||||
+50
-3
@@ -1,6 +1,7 @@
|
|||||||
schema {
|
schema {
|
||||||
query: Query
|
query: Query
|
||||||
mutation: Mutation
|
mutation: Mutation
|
||||||
|
subscription: Subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
@@ -25,6 +26,10 @@ type Query {
|
|||||||
posts(filter: PostsFilter): [Post!]!
|
posts(filter: PostsFilter): [Post!]!
|
||||||
|
|
||||||
|
|
||||||
|
"List unknown nicks"
|
||||||
|
unknownNicks(filter: UnknownNicksFilter): [UnknownNick!]!
|
||||||
|
|
||||||
|
|
||||||
# Find log by ID
|
# Find log by ID
|
||||||
log(id: String!): Log!
|
log(id: String!): Log!
|
||||||
|
|
||||||
@@ -36,8 +41,12 @@ type Query {
|
|||||||
chapter(id: String!): Chapter!
|
chapter(id: String!): Chapter!
|
||||||
|
|
||||||
|
|
||||||
|
# Find comment by ID
|
||||||
|
comment(id: String!): Comment!
|
||||||
|
|
||||||
|
|
||||||
# Find all distinct tags used in stories
|
# Find all distinct tags used in stories
|
||||||
tags: [Tag!]!
|
tags(filter: TagFilter): [Tag!]!
|
||||||
|
|
||||||
|
|
||||||
# Find story by ID
|
# Find story by ID
|
||||||
@@ -92,9 +101,28 @@ type Mutation {
|
|||||||
removeChapter(input: ChapterRemoveInput!): Chapter!
|
removeChapter(input: ChapterRemoveInput!): Chapter!
|
||||||
|
|
||||||
|
|
||||||
|
"Add a comment to a chapter."
|
||||||
|
addComment(input: CommentAddInput!): Comment!
|
||||||
|
|
||||||
|
"Edit a comment in a chapter."
|
||||||
|
editComment(input: CommentEditInput!): Comment!
|
||||||
|
|
||||||
|
"Remove a comment in a chapter."
|
||||||
|
removeComment(input: CommentRemoveInput!): Comment!
|
||||||
|
|
||||||
|
|
||||||
# Add a new log
|
# Add a new log
|
||||||
addLog(input: LogAddInput!): Log!
|
addLog(input: LogAddInput!): Log!
|
||||||
|
|
||||||
|
# Import a log
|
||||||
|
importLog(input: LogImportInput!): [Log!]!
|
||||||
|
|
||||||
|
# Split a log
|
||||||
|
splitLog(input: LogSplitInput!): Log!
|
||||||
|
|
||||||
|
# Merge a log
|
||||||
|
mergeLog(input: LogMergeInput!): Log!
|
||||||
|
|
||||||
# Edit a log
|
# Edit a log
|
||||||
editLog(input: LogEditInput!): Log!
|
editLog(input: LogEditInput!): Log!
|
||||||
|
|
||||||
@@ -136,8 +164,27 @@ type Mutation {
|
|||||||
|
|
||||||
# Edit a channel
|
# Edit a channel
|
||||||
editChannel(input: ChannelEditInput!): Channel!
|
editChannel(input: ChannelEditInput!): Channel!
|
||||||
|
|
||||||
|
|
||||||
|
"Upload a file"
|
||||||
|
uploadFile(input: FileUploadInput): File!
|
||||||
|
|
||||||
|
"Edit a file"
|
||||||
|
editFile(input: FileEditInput): File!
|
||||||
|
|
||||||
|
"Remove a file"
|
||||||
|
removeFile(input: FileRemoveInput): File!
|
||||||
}
|
}
|
||||||
|
|
||||||
# A Date represents a RFC3339 encoded date with up to millisecond precision.
|
type Subscription {
|
||||||
scalar Date
|
"""
|
||||||
|
Changes subscribes to the changes matching the following keys.
|
||||||
|
"""
|
||||||
|
changes(filter: ChangesFilter): Change!
|
||||||
|
}
|
||||||
|
|
||||||
|
# A Time represents a RFC3339 encoded date with up to millisecond precision.
|
||||||
|
scalar Time
|
||||||
|
|
||||||
|
"""A GraphQL file upload."""
|
||||||
|
scalar Upload
|
||||||
@@ -21,13 +21,13 @@ type Change {
|
|||||||
keys: [ChangeKey!]!
|
keys: [ChangeKey!]!
|
||||||
|
|
||||||
"The date when the change happened."
|
"The date when the change happened."
|
||||||
date: Date
|
date: Time
|
||||||
|
|
||||||
"The changed objects."
|
"The changed objects."
|
||||||
objects: [ChangeObject!]!
|
objects: [ChangeObject!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter
|
union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter | Comment | File
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A change key is a key associated with a change, used for filtering
|
A change key is a key associated with a change, used for filtering
|
||||||
@@ -64,6 +64,8 @@ enum ChangeModel {
|
|||||||
Story
|
Story
|
||||||
Tag
|
Tag
|
||||||
Chapter
|
Chapter
|
||||||
|
Comment
|
||||||
|
File
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -73,9 +75,12 @@ input ChangesFilter {
|
|||||||
"The keys to query for."
|
"The keys to query for."
|
||||||
keys: [ChangeKeyInput!]
|
keys: [ChangeKeyInput!]
|
||||||
|
|
||||||
"Only show changes more recent than this date."
|
"Only show changes by this author"
|
||||||
earliestDate: Date
|
author: String
|
||||||
|
|
||||||
"Limit the amount of results."
|
"Only show changes more recent than this date."
|
||||||
|
earliestDate: Time
|
||||||
|
|
||||||
|
"Limit the amount of results. This even goes for subscriptions!"
|
||||||
limit: Int
|
limit: Int
|
||||||
}
|
}
|
||||||
@@ -13,13 +13,42 @@ type Chapter {
|
|||||||
source: String!
|
source: String!
|
||||||
|
|
||||||
# When the chapter was posted initialy
|
# When the chapter was posted initialy
|
||||||
createdDate: Date!
|
createdDate: Time!
|
||||||
|
|
||||||
# The in-universe date and time of the chapter
|
# The in-universe date and time of the chapter
|
||||||
fictionalDate: Date
|
fictionalDate: Time
|
||||||
|
|
||||||
# The date of edit.
|
# The date of edit.
|
||||||
editedDate: Date!
|
editedDate: Time!
|
||||||
|
|
||||||
|
"The comment mode."
|
||||||
|
commentMode: ChapterCommentMode!
|
||||||
|
|
||||||
|
"Whether the author has locket comments."
|
||||||
|
commentsLocked: Boolean!
|
||||||
|
|
||||||
|
"A shorthand for checking whether a logged-in user can comment on this chapter."
|
||||||
|
canComment: Boolean!
|
||||||
|
|
||||||
|
"Get all chapter comments."
|
||||||
|
comments(limit: Int): [Comment!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The possibles modes of comments for a chapter.
|
||||||
|
"""
|
||||||
|
enum ChapterCommentMode {
|
||||||
|
"Comments are disabled and hidden."
|
||||||
|
Disabled
|
||||||
|
|
||||||
|
"Comments should be shown as typical news article comments."
|
||||||
|
Article
|
||||||
|
|
||||||
|
"Comments should be shown in a compact form suitable for many small messages."
|
||||||
|
Chat
|
||||||
|
|
||||||
|
"Comments are going to be shown as omni-tool messages."
|
||||||
|
Message
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for addChapter mutation
|
# Input for addChapter mutation
|
||||||
@@ -40,7 +69,10 @@ input ChapterAddInput {
|
|||||||
source: String!
|
source: String!
|
||||||
|
|
||||||
# Optionally, assign a fictional date for the chapter
|
# Optionally, assign a fictional date for the chapter
|
||||||
fictionalDate: Date
|
fictionalDate: Time
|
||||||
|
|
||||||
|
"The comment mode for the chapter."
|
||||||
|
commentMode: ChapterCommentMode
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for editChapter mutation
|
# Input for editChapter mutation
|
||||||
@@ -55,10 +87,16 @@ input ChapterEditInput {
|
|||||||
source: String
|
source: String
|
||||||
|
|
||||||
# Set the fictional date for a chapter
|
# Set the fictional date for a chapter
|
||||||
fictionalDate: Date
|
fictionalDate: Time
|
||||||
|
|
||||||
# Remove the fictional date for a chapter
|
# Remove the fictional date for a chapter
|
||||||
clearFictionalDate: Boolean
|
clearFictionalDate: Boolean
|
||||||
|
|
||||||
|
"Set the comment mode for the chapter"
|
||||||
|
commentMode: ChapterCommentMode
|
||||||
|
|
||||||
|
"Set whether the chapter comments are locked"
|
||||||
|
commentsLocked: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for moveChapter mutation
|
# Input for moveChapter mutation
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
A Comment represents a comment to a story chapter.
|
||||||
|
"""
|
||||||
|
type Comment {
|
||||||
|
"A unique ID of the change."
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
"subject"
|
||||||
|
subject: String!
|
||||||
|
|
||||||
|
"The chapter ID the comment belongs to."
|
||||||
|
chapterId: String!
|
||||||
|
|
||||||
|
"The comment's author."
|
||||||
|
author: String!
|
||||||
|
|
||||||
|
"The displayed name of the character. This may be the same as character.name, but it does not need to be."
|
||||||
|
characterName: String!
|
||||||
|
|
||||||
|
"The character associated with the comment."
|
||||||
|
character: Character
|
||||||
|
|
||||||
|
"The fictional (IC) date of the comment."
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
"The date of creation."
|
||||||
|
createdDate: Time!
|
||||||
|
|
||||||
|
"The date of the last edit."
|
||||||
|
editedDate: Time!
|
||||||
|
|
||||||
|
"The markdown source of the comment."
|
||||||
|
source: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for the addComment mutation
|
||||||
|
"""
|
||||||
|
input CommentAddInput {
|
||||||
|
"What chapter to comment on."
|
||||||
|
chapterId: String!
|
||||||
|
|
||||||
|
"The markdown source of the comment."
|
||||||
|
source: String!
|
||||||
|
|
||||||
|
"The name of the character associated with this comment."
|
||||||
|
characterName: String!
|
||||||
|
|
||||||
|
"Optioanlly, a character ID."
|
||||||
|
characterId: String
|
||||||
|
|
||||||
|
"Optional in-universe date of the comment."
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
"Optional subject to add to the comment, only shown in some modes."
|
||||||
|
subject: String
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for the editComment mutation
|
||||||
|
"""
|
||||||
|
input CommentEditInput {
|
||||||
|
"What comment to edit."
|
||||||
|
commentId: String!
|
||||||
|
|
||||||
|
"Change the markdown source."
|
||||||
|
source: String
|
||||||
|
|
||||||
|
"Change the name of the character associated with this comment."
|
||||||
|
characterName: String
|
||||||
|
|
||||||
|
"Change the character assocaited with this comment."
|
||||||
|
characterId: String
|
||||||
|
|
||||||
|
"Change the optional in-universe date of the comment."
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
"Set to clear the fictional date."
|
||||||
|
clearFictionalDate: Boolean
|
||||||
|
|
||||||
|
"Change the optional subject for the comment, only shown in some modes."
|
||||||
|
subject: String
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for the removeComment mutation
|
||||||
|
"""
|
||||||
|
input CommentRemoveInput {
|
||||||
|
"What comment to edit."
|
||||||
|
commentId: String!
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ type File {
|
|||||||
kind: String!
|
kind: String!
|
||||||
|
|
||||||
# The time of uploading
|
# The time of uploading
|
||||||
time: Date!
|
time: Time!
|
||||||
|
|
||||||
# Whether the file is publicly listable. Someone with knowledge of the ID
|
# Whether the file is publicly listable. Someone with knowledge of the ID
|
||||||
# will still be able to view it, however.
|
# will still be able to view it, however.
|
||||||
@@ -35,11 +35,23 @@ input FilesFilter {
|
|||||||
public: Boolean
|
public: Boolean
|
||||||
|
|
||||||
# Limit the MIME types of the files.
|
# Limit the MIME types of the files.
|
||||||
mimeType: [String!]
|
mimeTypes: [String!]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for uploadFile mutation
|
||||||
|
input FileUploadInput {
|
||||||
|
"The file name"
|
||||||
|
name: String!
|
||||||
|
|
||||||
|
# Whether the file should be public
|
||||||
|
public: Boolean!
|
||||||
|
|
||||||
|
# The actual file upload
|
||||||
|
file: Upload!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for editFile mutation
|
# Input for editFile mutation
|
||||||
input EditFileInput {
|
input FileEditInput {
|
||||||
# The file's unique ID
|
# The file's unique ID
|
||||||
id: String!
|
id: String!
|
||||||
|
|
||||||
@@ -50,3 +62,9 @@ input EditFileInput {
|
|||||||
# The file's name
|
# The file's name
|
||||||
name: String
|
name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Input for removeFile mutation
|
||||||
|
input FileRemoveInput {
|
||||||
|
# The file's unique ID
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ type Log {
|
|||||||
shortId: String!
|
shortId: String!
|
||||||
|
|
||||||
# The date for a log.
|
# The date for a log.
|
||||||
date: Date!
|
date: Time!
|
||||||
|
|
||||||
# The channel of a log.
|
# The channel of a log.
|
||||||
channelName: String!
|
channelName: String!
|
||||||
@@ -34,6 +34,20 @@ type Log {
|
|||||||
|
|
||||||
# The posts of the logfile, which can be filtered by kinds.
|
# The posts of the logfile, which can be filtered by kinds.
|
||||||
posts(kinds:[String!]): [Post!]!
|
posts(kinds:[String!]): [Post!]!
|
||||||
|
|
||||||
|
# Suggested next logs.
|
||||||
|
nextLogs: [LogSuggestion!]!
|
||||||
|
|
||||||
|
# Suggested previous logs.
|
||||||
|
prevLogs: [LogSuggestion!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Suggested log
|
||||||
|
type LogSuggestion {
|
||||||
|
log: Log!
|
||||||
|
hasEvent: Boolean!
|
||||||
|
hasChannel: Boolean!
|
||||||
|
characters: [Character!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Filter for logs query
|
# Filter for logs query
|
||||||
@@ -60,7 +74,7 @@ input LogsFilter {
|
|||||||
# Input for addLog mutation
|
# Input for addLog mutation
|
||||||
input LogAddInput {
|
input LogAddInput {
|
||||||
# The date of the log.
|
# The date of the log.
|
||||||
date: Date!
|
date: Time!
|
||||||
|
|
||||||
# The channel where the log is set
|
# The channel where the log is set
|
||||||
channel: String!
|
channel: String!
|
||||||
@@ -101,3 +115,68 @@ input LogRemoveInput {
|
|||||||
# The id of the log to remove
|
# The id of the log to remove
|
||||||
id: String!
|
id: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for importLog mutation.
|
||||||
|
"""
|
||||||
|
input LogImportInput {
|
||||||
|
"The channel of the log, alwyas required."
|
||||||
|
channelName: String!
|
||||||
|
|
||||||
|
"Which importer to use."
|
||||||
|
importer: LogImporter!
|
||||||
|
|
||||||
|
"The timezone of the import, all log and post dates will be treated as it. It will be UTC if none is specified."
|
||||||
|
timezone: String
|
||||||
|
|
||||||
|
"The date of the log, if not provided in the log body."
|
||||||
|
date: Time
|
||||||
|
|
||||||
|
"Session threwhold"
|
||||||
|
sessionThresholdMs: Int
|
||||||
|
|
||||||
|
"The log body itself."
|
||||||
|
data: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for splitLog mutation.
|
||||||
|
"""
|
||||||
|
input LogSplitInput {
|
||||||
|
"Log ID"
|
||||||
|
logId: String!
|
||||||
|
|
||||||
|
"Split out all posts from and including the post with this ID"
|
||||||
|
startPostId: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for mergeLog mutation.
|
||||||
|
"""
|
||||||
|
input LogMergeInput {
|
||||||
|
"Target log ID"
|
||||||
|
targetLogId: String!
|
||||||
|
|
||||||
|
"Source log ID"
|
||||||
|
sourceLogId: String!
|
||||||
|
|
||||||
|
"Remove the source log after merging"
|
||||||
|
removeAfter: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LogImporter {
|
||||||
|
"""
|
||||||
|
mIRC-like: This importer parses logs that copied from mIRC posts without spaces.
|
||||||
|
"""
|
||||||
|
MircLike
|
||||||
|
|
||||||
|
"""
|
||||||
|
Forum log: This importer parses the format on the forum. The displayed log, not the post source.
|
||||||
|
"""
|
||||||
|
ForumLog
|
||||||
|
|
||||||
|
"""
|
||||||
|
IRCCloud: This importer parses irccloud exports.
|
||||||
|
"""
|
||||||
|
IrcCloud
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ type Post {
|
|||||||
logId: String!
|
logId: String!
|
||||||
|
|
||||||
# The date and time of posting
|
# The date and time of posting
|
||||||
time: Date!
|
time: Time!
|
||||||
|
|
||||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||||
kind: String!
|
kind: String!
|
||||||
@@ -28,7 +28,7 @@ input PostAddInput {
|
|||||||
logId: String!
|
logId: String!
|
||||||
|
|
||||||
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
||||||
time: Date!
|
time: Time!
|
||||||
|
|
||||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||||
kind: String!
|
kind: String!
|
||||||
@@ -46,7 +46,7 @@ input PostEditInput {
|
|||||||
id: String!
|
id: String!
|
||||||
|
|
||||||
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
||||||
time: Date
|
time: Time
|
||||||
|
|
||||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||||
kind: String
|
kind: String
|
||||||
@@ -75,8 +75,8 @@ input PostRemoveInput {
|
|||||||
|
|
||||||
# Filter for posts query
|
# Filter for posts query
|
||||||
input PostsFilter {
|
input PostsFilter {
|
||||||
id: [String!]
|
ids: [String!]
|
||||||
kind: [String!]
|
kinds: [String!]
|
||||||
logId: String
|
logId: String
|
||||||
limit: Int
|
limit: Int
|
||||||
}
|
}
|
||||||
@@ -26,13 +26,16 @@ type Story {
|
|||||||
chapters: [Chapter!]!
|
chapters: [Chapter!]!
|
||||||
|
|
||||||
# The date the story was created.
|
# The date the story was created.
|
||||||
createdDate: Date!
|
createdDate: Time!
|
||||||
|
|
||||||
# The date the story is set in.
|
# The date the story is set in.
|
||||||
fictionalDate: Date
|
fictionalDate: Time
|
||||||
|
|
||||||
# The date of the last major update to the story. This being bumped means a new chapter has been added.
|
# The date of the last major update to the story. This being bumped means a new chapter has been added.
|
||||||
updatedDate: Date!
|
updatedDate: Time!
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Filter for stories query.
|
# Filter for stories query.
|
||||||
@@ -53,10 +56,10 @@ input StoriesFilter {
|
|||||||
open: Boolean
|
open: Boolean
|
||||||
|
|
||||||
# The earliest fictionalDate
|
# The earliest fictionalDate
|
||||||
earliestFictionalDate: Date
|
earliestFictionalDate: Time
|
||||||
|
|
||||||
# The latest fictionalDate
|
# The latest fictionalDate
|
||||||
latestFictionalDate: Date
|
latestFictionalDate: Time
|
||||||
|
|
||||||
# The max amount of stories to get (default: 30)
|
# The max amount of stories to get (default: 30)
|
||||||
limit: Int
|
limit: Int
|
||||||
@@ -85,7 +88,10 @@ input StoryAddInput {
|
|||||||
tags: [TagInput!]
|
tags: [TagInput!]
|
||||||
|
|
||||||
# Set the fictional date of the story.
|
# Set the fictional date of the story.
|
||||||
fictionalDate: Date
|
fictionalDate: Time
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for the editStory mutation
|
# Input for the editStory mutation
|
||||||
@@ -111,10 +117,13 @@ input StoryEditInput {
|
|||||||
listed: Boolean
|
listed: Boolean
|
||||||
|
|
||||||
# Set the fictional date of the story.
|
# Set the fictional date of the story.
|
||||||
fictionalDate: Date
|
fictionalDate: Time
|
||||||
|
|
||||||
# Clear the fictional date of the story.
|
# Clear the fictional date of the story.
|
||||||
clearFictionalDate: Boolean
|
clearFictionalDate: Boolean
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for the addStoryTag mutation
|
# Input for the addStoryTag mutation
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ input TagInput {
|
|||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
A TagFilter is a filter for the tags query.
|
||||||
|
"""
|
||||||
|
input TagFilter {
|
||||||
|
"Filter by kind"
|
||||||
|
kind: TagKind
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Allowed values for Tag.kind
|
# Allowed values for Tag.kind
|
||||||
enum TagKind {
|
enum TagKind {
|
||||||
# An organization is a catch all term for in-universe corporations, teams, groups, cults, forces, etc...
|
# An organization is a catch all term for in-universe corporations, teams, groups, cults, forces, etc...
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
An unknown nick is a nick found by the logbot that's not associated with
|
||||||
|
any characters. Many are going to be false positives, but most should be
|
||||||
|
useful in pointing out which characters are missing.
|
||||||
|
"""
|
||||||
|
type UnknownNick {
|
||||||
|
"The IRC nick that's unknown."
|
||||||
|
nick: String!
|
||||||
|
|
||||||
|
"The amount of logs that contains the unknown nick."
|
||||||
|
score: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for the unknownNicks query.
|
||||||
|
"""
|
||||||
|
input UnknownNicksFilter {
|
||||||
|
"The highest amount of unknown nicks to return."
|
||||||
|
limit: Int!
|
||||||
|
}
|
||||||
+10
-5
@@ -2,19 +2,24 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/graph2/input"
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type changeResolver struct{}
|
type changeResolver struct{}
|
||||||
|
|
||||||
func (r *changeResolver) Objects(ctx context.Context, obj *models.Change) ([]input.ChangeObject, error) {
|
func (r *changeResolver) Objects(ctx context.Context, obj *models.Change) ([]graphcore.ChangeObject, error) {
|
||||||
objects := obj.Objects()
|
objects := obj.Objects()
|
||||||
|
|
||||||
results := make([]input.ChangeObject, len(objects))
|
results := make([]graphcore.ChangeObject, 0, len(objects))
|
||||||
for i := range objects {
|
for _, object := range objects {
|
||||||
results[i] = objects[i]
|
if changeObject, ok := object.(graphcore.ChangeObject); ok {
|
||||||
|
results = append(results, changeObject)
|
||||||
|
} else {
|
||||||
|
log.Printf("Type %T needs ChangeObject impl", object)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
|
|||||||
+16
-2
@@ -2,12 +2,15 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type chapterResolver struct{}
|
type chapterResolver struct {
|
||||||
|
stories *services.StoryService
|
||||||
|
}
|
||||||
|
|
||||||
func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Chapter) (*time.Time, error) {
|
func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Chapter) (*time.Time, error) {
|
||||||
if chapter.FictionalDate.IsZero() {
|
if chapter.FictionalDate.IsZero() {
|
||||||
@@ -17,5 +20,16 @@ func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Cha
|
|||||||
return &chapter.FictionalDate, nil
|
return &chapter.FictionalDate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *chapterResolver) Comments(ctx context.Context, chapter *models.Chapter, limit *int) ([]*models.Comment, error) {
|
||||||
|
if limit == nil {
|
||||||
|
limit = new(int)
|
||||||
|
*limit = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.stories.ListComments(ctx, *chapter, *limit)
|
||||||
|
}
|
||||||
|
|
||||||
// ChapterResolver is a resolver
|
// ChapterResolver is a resolver
|
||||||
var ChapterResolver chapterResolver
|
func ChapterResolver(s *services.Bundle) *chapterResolver {
|
||||||
|
return &chapterResolver{stories: s.Stories}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commentResolver struct {
|
||||||
|
characters *services.CharacterService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *commentResolver) Character(ctx context.Context, obj *models.Comment) (*models.Character, error) {
|
||||||
|
if obj.CharacterID == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
character, err := r.characters.Find(ctx, obj.CharacterID)
|
||||||
|
if err != nil {
|
||||||
|
if err == repositories.ErrNotFound || err == mgo.ErrNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return character, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *commentResolver) FictionalDate(ctx context.Context, obj *models.Comment) (*time.Time, error) {
|
||||||
|
if obj.FictionalDate.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj.FictionalDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentResolver is a resolver
|
||||||
|
func CommentResolver(s *services.Bundle) *commentResolver {
|
||||||
|
return &commentResolver{characters: s.Characters}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
+27
-23
@@ -2,40 +2,44 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/loader"
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/posts"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type logResolver struct{}
|
type logResolver struct {
|
||||||
|
logs *services.LogService
|
||||||
func (r *logResolver) Channel(ctx context.Context, log *models.Log) (models.Channel, error) {
|
characters *services.CharacterService
|
||||||
loader := loader.FromContext(ctx)
|
channels *services.ChannelService
|
||||||
if loader == nil {
|
|
||||||
return models.Channel{}, errors.New("no loader")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return loader.Channel("name", log.ChannelName)
|
func (r *logResolver) Channel(ctx context.Context, log *models.Log) (*models.Channel, error) {
|
||||||
|
return r.channels.Find(ctx, log.ChannelName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *logResolver) Characters(ctx context.Context, log *models.Log) ([]models.Character, error) {
|
func (r *logResolver) Characters(ctx context.Context, log *models.Log) ([]*models.Character, error) {
|
||||||
loader := loader.FromContext(ctx)
|
if len(log.CharacterIDs) == 0 {
|
||||||
if loader == nil {
|
return []*models.Character{}, nil
|
||||||
return nil, errors.New("no loader")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return loader.Characters("id", log.CharacterIDs...)
|
return r.characters.List(ctx, models.CharacterFilter{
|
||||||
}
|
IDs: 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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *logResolver) Posts(ctx context.Context, log *models.Log, kinds []string) ([]*models.Post, error) {
|
||||||
|
return r.logs.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logResolver) NextLogs(ctx context.Context, obj *models.Log) ([]*models.LogSuggestion, error) {
|
||||||
|
return r.logs.NextLogs(ctx, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *logResolver) PrevLogs(ctx context.Context, obj *models.Log) ([]*models.LogSuggestion, error) {
|
||||||
|
return r.logs.PrevLogs(ctx, obj)
|
||||||
|
}
|
||||||
|
|
||||||
// LogResolver is a resolver
|
// LogResolver is a resolver
|
||||||
var LogResolver logResolver
|
func LogResolver(s *services.Bundle) *logResolver {
|
||||||
|
return &logResolver{characters: s.Characters, logs: s.Logs, channels: s.Channels}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/chapters"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type storyResolver struct{}
|
type storyResolver struct {
|
||||||
|
stories *services.StoryService
|
||||||
|
}
|
||||||
|
|
||||||
func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) {
|
func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) {
|
||||||
if story.FictionalDate.IsZero() {
|
if story.FictionalDate.IsZero() {
|
||||||
@@ -19,9 +20,11 @@ func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story)
|
|||||||
return &story.FictionalDate, nil
|
return &story.FictionalDate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]models.Chapter, error) {
|
func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]*models.Chapter, error) {
|
||||||
return chapters.ListStoryID(story.ID)
|
return r.stories.ListChapters(ctx, *story)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoryResolver is a resolver
|
// StoryResolver is a resolver
|
||||||
var StoryResolver storyResolver
|
func StoryResolver(s *services.Bundle) *storyResolver {
|
||||||
|
return &storyResolver{stories: s.Stories}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/users"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type tokenResolver struct{}
|
type tokenResolver struct {
|
||||||
|
auth *services.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
func (r *tokenResolver) User(ctx context.Context, token *models.Token) (models.User, error) {
|
func (r *tokenResolver) User(ctx context.Context, token *models.Token) (*models.User, error) {
|
||||||
return users.Find(token.UserID)
|
return r.auth.FindUser(ctx, token.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenResolver is a resolver
|
// TokenResolver is a resolver
|
||||||
var TokenResolver tokenResolver
|
func TokenResolver(s *services.Bundle) *tokenResolver {
|
||||||
|
return &tokenResolver{auth: s.Auth}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
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,29 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
// AllPermissions gets all permissions and their purpose
|
|
||||||
func AllPermissions() map[string]string {
|
|
||||||
return map[string]string{
|
|
||||||
"member": "Can add/edit/remove own content",
|
|
||||||
"user.edit": "Can edit non-owned users",
|
|
||||||
"character.add": "Can add non-owned characters",
|
|
||||||
"character.edit": "Can edit non-owned characters",
|
|
||||||
"character.remove": "Can remove non-owned characters",
|
|
||||||
"channel.add": "Can add channels",
|
|
||||||
"channel.edit": "Can edit channels",
|
|
||||||
"channel.remove": "Can remove channels",
|
|
||||||
"log.add": "Can add logs",
|
|
||||||
"log.edit": "Can edit logs",
|
|
||||||
"log.remove": "Can remove logs",
|
|
||||||
"post.add": "Can add posts",
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
+52
-24
@@ -5,55 +5,83 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalMutex sync.Mutex
|
var globalMutex sync.Mutex
|
||||||
var global *Config
|
var global *Config
|
||||||
|
|
||||||
// Config is configuration
|
// Config is configuration data
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Space struct {
|
Space Space `json:"space" yaml:"space"`
|
||||||
Host string `json:"host"`
|
Database Database `json:"database" yaml:"database"`
|
||||||
AccessKey string `json:"accessKey"`
|
Wiki Wiki `json:"wiki" yaml:"wiki"`
|
||||||
SecretKey string `json:"secretKey"`
|
}
|
||||||
Bucket string `json:"bucket"`
|
|
||||||
MaxSize int64 `json:"maxSize"`
|
|
||||||
Root string `json:"root"`
|
|
||||||
} `json:"space"`
|
|
||||||
|
|
||||||
Database struct {
|
// Space is configuration for spaces.
|
||||||
Host string `json:"host"`
|
type Space struct {
|
||||||
Port int `json:"port"`
|
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||||
Db string `json:"db"`
|
Host string `json:"host" yaml:"host"`
|
||||||
Username string `json:"username"`
|
AccessKey string `json:"accessKey" yaml:"accessKey"`
|
||||||
Password string `json:"password"`
|
SecretKey string `json:"secretKey" yaml:"secretKey"`
|
||||||
Mechanism string `json:"mechanism"`
|
Bucket string `json:"bucket" yaml:"bucket"`
|
||||||
} `json:"database"`
|
MaxSize int64 `json:"maxSize" yaml:"maxSize"`
|
||||||
|
Root string `json:"root" yaml:"root"`
|
||||||
|
URLRoot string `json:"urlRoot" yaml:"urlRoot"`
|
||||||
|
}
|
||||||
|
|
||||||
Wiki struct {
|
// Database is configuration for spaces.
|
||||||
URL string `json:"url"`
|
type Database struct {
|
||||||
} `json:"wiki"`
|
Driver string `json:"driver" yaml:"driver"`
|
||||||
|
Host string `json:"host" yaml:"host"`
|
||||||
|
Port int `json:"port" yaml:"port"`
|
||||||
|
Db string `json:"db" yaml:"db"`
|
||||||
|
Username string `json:"username" yaml:"username"`
|
||||||
|
Password string `json:"password" yaml:"password"`
|
||||||
|
Mechanism string `json:"mechanism" yaml:"mechanism"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wiki is the wiki stuff.
|
||||||
|
type Wiki struct {
|
||||||
|
URL string `json:"url" yaml:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads config stuff
|
// Load loads config stuff
|
||||||
func (config *Config) Load(filename string) error {
|
func (config *Config) Load(filename string) error {
|
||||||
|
log.Println("Trying to load config from " + filename)
|
||||||
|
|
||||||
|
stat, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Size() < 1 {
|
||||||
|
return errors.New("File is empty")
|
||||||
|
}
|
||||||
|
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(filename, ".json") {
|
||||||
return json.NewDecoder(file).Decode(config)
|
return json.NewDecoder(file).Decode(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return yaml.NewDecoder(file).Decode(config)
|
||||||
|
}
|
||||||
|
|
||||||
// LoadAny loads the first of these files it can find
|
// LoadAny loads the first of these files it can find
|
||||||
func (config *Config) LoadAny(filenames ...string) error {
|
func (config *Config) LoadAny(filenames ...string) error {
|
||||||
for _, filename := range filenames {
|
for _, filename := range filenames {
|
||||||
if err := config.Load(filename); err == nil {
|
if err := config.Load(filename); err != nil {
|
||||||
return nil
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
*config = Config{}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("Failed to load configuration files")
|
return errors.New("Failed to load configuration files")
|
||||||
@@ -64,7 +92,7 @@ func Global() Config {
|
|||||||
globalMutex.Lock()
|
globalMutex.Lock()
|
||||||
if global == nil {
|
if global == nil {
|
||||||
global = &Config{}
|
global = &Config{}
|
||||||
err := global.LoadAny("/etc/aiterp/rpdata.json", "./config.json")
|
err := global.LoadAny("/etc/aiterp/rpdata.yaml", "/etc/aiterp/rpdata.json", "./config.yaml", "./config.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package counter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var collection *mgo.Collection
|
|
||||||
|
|
||||||
type counter struct {
|
|
||||||
ID string `bson:"_id"`
|
|
||||||
Value int `bson:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next gets the next value of a counter, or an error if it hasn't.
|
|
||||||
func Next(category, name string) (int, error) {
|
|
||||||
id := category + "." + name
|
|
||||||
doc := counter{}
|
|
||||||
|
|
||||||
_, err := collection.Find(bson.M{"_id": id}).Apply(mgo.Change{
|
|
||||||
Update: bson.M{"$inc": bson.M{"value": 1}},
|
|
||||||
Upsert: true,
|
|
||||||
ReturnNew: true,
|
|
||||||
}, &doc)
|
|
||||||
if err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
collection = db.C("core.counters")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package generate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
mrand "math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ID generates an ID using crypto-random, falling back to math random when that fails
|
||||||
|
// to avoid disrupting operation because of a faulty RNG.
|
||||||
|
func ID(prefix string, length int) string {
|
||||||
|
var data [32]byte
|
||||||
|
|
||||||
|
result := strings.Builder{}
|
||||||
|
result.Grow(length + 32)
|
||||||
|
result.WriteString(prefix)
|
||||||
|
|
||||||
|
pos := 0
|
||||||
|
for result.Len() < length {
|
||||||
|
if pos == 0 {
|
||||||
|
randRead(data[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
result.WriteString(strconv.FormatUint(binary.BigEndian.Uint64(data[pos:pos+8]), 36))
|
||||||
|
|
||||||
|
pos = (pos + 8) % 32
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String()[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
func randRead(data []byte) {
|
||||||
|
n, err := rand.Read(data)
|
||||||
|
if err != nil {
|
||||||
|
mrand.Read(data[n:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostID generates a post ID.
|
||||||
|
func PostID() string {
|
||||||
|
return ID("P", 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoryID generates a post ID.
|
||||||
|
func StoryID() string {
|
||||||
|
return ID("S", 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChapterID generates a post ID.
|
||||||
|
func ChapterID() string {
|
||||||
|
return ID("SC", 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentID generates a post ID.
|
||||||
|
func CommentID() string {
|
||||||
|
return ID("SCC", 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileID generates a file ID.
|
||||||
|
func FileID() string {
|
||||||
|
return ID("F", 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileID generates a file ID.
|
||||||
|
func FileUploadID() string {
|
||||||
|
return ID("U", 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyID() string {
|
||||||
|
return ID("K", 32)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package generate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogID generates a log ID in the format is 2019-05-22_191341547_RedrockAgency
|
||||||
|
func LogID(log models.Log) string {
|
||||||
|
if len(log.ChannelName) < 1 {
|
||||||
|
panic("ChannelName is not valid (validate input before calling this function!)")
|
||||||
|
}
|
||||||
|
|
||||||
|
datetime := log.Date.UTC().Format("2006-01-02_150405")
|
||||||
|
milliseconds := (log.Date.UnixNano() % int64(time.Second)) / 1000000
|
||||||
|
channelName := log.ChannelName[1:]
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s%03d_%s", datetime, milliseconds, channelName)
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
package instrumentation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
|
prometheusclient "github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
existStatusFailure = "failure"
|
||||||
|
exitStatusSuccess = "success"
|
||||||
|
)
|
||||||
|
|
||||||
|
var latencyMapMutex sync.Mutex
|
||||||
|
var latencyMap = make(map[string]float64, 128)
|
||||||
|
|
||||||
|
type ctxKeyType string
|
||||||
|
|
||||||
|
var ctxKey = ctxKeyType("metricglobal")
|
||||||
|
|
||||||
|
var (
|
||||||
|
requestStartedCounter prometheusclient.Counter
|
||||||
|
requestCompletedCounter prometheusclient.Counter
|
||||||
|
requestFailureCounter *prometheusclient.CounterVec
|
||||||
|
requestSuccessCounter *prometheusclient.CounterVec
|
||||||
|
requestComplexityHisto prometheusclient.Histogram
|
||||||
|
resolverStartedCounter *prometheusclient.CounterVec
|
||||||
|
resolverCompletedCounter *prometheusclient.CounterVec
|
||||||
|
timeToResolveField *prometheusclient.HistogramVec
|
||||||
|
timeToHandleRequest *prometheusclient.HistogramVec
|
||||||
|
timeToHandleRequestQueryMAvg *prometheusclient.GaugeVec
|
||||||
|
timeToHandleRequestQueryTotal *prometheusclient.CounterVec
|
||||||
|
responseSizeMAvg *prometheusclient.GaugeVec
|
||||||
|
responseSizeTotal *prometheusclient.CounterVec
|
||||||
|
)
|
||||||
|
|
||||||
|
// Globals is monitoring data.
|
||||||
|
type Globals struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
Queries []string
|
||||||
|
Failed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register registers the prometheus metrics.
|
||||||
|
func Register() {
|
||||||
|
requestStartedCounter = prometheusclient.NewCounter(
|
||||||
|
prometheusclient.CounterOpts{
|
||||||
|
Name: "graphql_request_started_total",
|
||||||
|
Help: "Total number of requests started on the graphql server.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
requestCompletedCounter = prometheusclient.NewCounter(
|
||||||
|
prometheusclient.CounterOpts{
|
||||||
|
Name: "graphql_request_completed_total",
|
||||||
|
Help: "Total number of requests completed on the graphql server.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
requestFailureCounter = prometheusclient.NewCounterVec(
|
||||||
|
prometheusclient.CounterOpts{
|
||||||
|
Name: "graphql_request_failures",
|
||||||
|
Help: "Number of requests with either success or failure status.",
|
||||||
|
},
|
||||||
|
[]string{"queries"},
|
||||||
|
)
|
||||||
|
requestSuccessCounter = prometheusclient.NewCounterVec(
|
||||||
|
prometheusclient.CounterOpts{
|
||||||
|
Name: "graphql_request_successes",
|
||||||
|
Help: "Number of requests with either success or failure status.",
|
||||||
|
},
|
||||||
|
[]string{"queries"},
|
||||||
|
)
|
||||||
|
|
||||||
|
requestComplexityHisto = prometheusclient.NewHistogram(
|
||||||
|
prometheusclient.HistogramOpts{
|
||||||
|
Name: "graphql_request_complexity",
|
||||||
|
Help: "The complexity value of incoming queries.",
|
||||||
|
Buckets: prometheusclient.LinearBuckets(100, 25, 9),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
resolverStartedCounter = prometheusclient.NewCounterVec(
|
||||||
|
prometheusclient.CounterOpts{
|
||||||
|
Name: "graphql_resolver_started_total",
|
||||||
|
Help: "Total number of resolver started on the graphql server.",
|
||||||
|
},
|
||||||
|
[]string{"object", "field"},
|
||||||
|
)
|
||||||
|
|
||||||
|
resolverCompletedCounter = prometheusclient.NewCounterVec(
|
||||||
|
prometheusclient.CounterOpts{
|
||||||
|
Name: "graphql_resolver_completed_total",
|
||||||
|
Help: "Total number of resolver completed on the graphql server.",
|
||||||
|
},
|
||||||
|
[]string{"object", "field"},
|
||||||
|
)
|
||||||
|
|
||||||
|
timeToResolveField = prometheusclient.NewHistogramVec(prometheusclient.HistogramOpts{
|
||||||
|
Name: "graphql_resolver_duration_ms",
|
||||||
|
Help: "The time taken to resolve a field by graphql server.",
|
||||||
|
Buckets: prometheusclient.ExponentialBuckets(1, 2, 11),
|
||||||
|
}, []string{"exitStatus"})
|
||||||
|
|
||||||
|
timeToHandleRequest = prometheusclient.NewHistogramVec(prometheusclient.HistogramOpts{
|
||||||
|
Name: "graphql_request_duration_ms",
|
||||||
|
Help: "The time taken to handle a request by graphql server.",
|
||||||
|
Buckets: prometheusclient.ExponentialBuckets(1, 2, 11),
|
||||||
|
}, []string{"exitStatus"})
|
||||||
|
|
||||||
|
timeToHandleRequestQueryMAvg = prometheusclient.NewGaugeVec(
|
||||||
|
prometheusclient.GaugeOpts{
|
||||||
|
Name: "graphql_request_query_duration_mavg_ms",
|
||||||
|
Help: "Moving average of graphql request query duration for a query set.",
|
||||||
|
},
|
||||||
|
[]string{"queries"},
|
||||||
|
)
|
||||||
|
timeToHandleRequestQueryTotal = prometheusclient.NewCounterVec(
|
||||||
|
prometheusclient.CounterOpts{
|
||||||
|
Name: "graphql_request_query_duration_total_ms",
|
||||||
|
Help: "Total duration spent responding to a query.",
|
||||||
|
},
|
||||||
|
[]string{"queries"},
|
||||||
|
)
|
||||||
|
|
||||||
|
responseSizeMAvg = prometheusclient.NewGaugeVec(
|
||||||
|
prometheusclient.GaugeOpts{
|
||||||
|
Name: "graphql_response_size_mavg_bytes",
|
||||||
|
Help: "Moving average of graphql response size for a query set.",
|
||||||
|
},
|
||||||
|
[]string{"queries"},
|
||||||
|
)
|
||||||
|
responseSizeTotal = prometheusclient.NewCounterVec(
|
||||||
|
prometheusclient.CounterOpts{
|
||||||
|
Name: "graphql_response_size_total_bytes",
|
||||||
|
Help: "Total bytes of response data to a query.",
|
||||||
|
},
|
||||||
|
[]string{"queries"},
|
||||||
|
)
|
||||||
|
|
||||||
|
prometheusclient.MustRegister(
|
||||||
|
requestStartedCounter,
|
||||||
|
requestCompletedCounter,
|
||||||
|
requestSuccessCounter,
|
||||||
|
requestFailureCounter,
|
||||||
|
requestComplexityHisto,
|
||||||
|
resolverStartedCounter,
|
||||||
|
resolverCompletedCounter,
|
||||||
|
timeToResolveField,
|
||||||
|
timeToHandleRequest,
|
||||||
|
timeToHandleRequestQueryMAvg,
|
||||||
|
timeToHandleRequestQueryTotal,
|
||||||
|
responseSizeMAvg,
|
||||||
|
responseSizeTotal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnRegister unregisters
|
||||||
|
func UnRegister() {
|
||||||
|
prometheusclient.Unregister(requestStartedCounter)
|
||||||
|
prometheusclient.Unregister(requestCompletedCounter)
|
||||||
|
prometheusclient.Unregister(requestSuccessCounter)
|
||||||
|
prometheusclient.Unregister(requestFailureCounter)
|
||||||
|
prometheusclient.Unregister(requestComplexityHisto)
|
||||||
|
prometheusclient.Unregister(resolverStartedCounter)
|
||||||
|
prometheusclient.Unregister(resolverCompletedCounter)
|
||||||
|
prometheusclient.Unregister(timeToResolveField)
|
||||||
|
prometheusclient.Unregister(timeToHandleRequest)
|
||||||
|
prometheusclient.Unregister(timeToHandleRequestQueryMAvg)
|
||||||
|
prometheusclient.Unregister(timeToHandleRequestQueryTotal)
|
||||||
|
prometheusclient.Unregister(responseSizeMAvg)
|
||||||
|
prometheusclient.Unregister(responseSizeTotal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolverMiddleware is a resolver middleware that logs metrics for prometheus.
|
||||||
|
func ResolverMiddleware() graphql.FieldMiddleware {
|
||||||
|
return func(ctx context.Context, next graphql.Resolver) (interface{}, error) {
|
||||||
|
rctx := graphql.GetResolverContext(ctx)
|
||||||
|
|
||||||
|
resolverStartedCounter.WithLabelValues(rctx.Object, rctx.Field.Name).Inc()
|
||||||
|
|
||||||
|
observerStart := time.Now()
|
||||||
|
|
||||||
|
path := rctx.Path()
|
||||||
|
if len(path) == 1 {
|
||||||
|
if globals, ok := ctx.Value(ctxKey).(*Globals); ok {
|
||||||
|
globals.Lock()
|
||||||
|
if rctx.Field.Field != nil {
|
||||||
|
globals.Queries = append(globals.Queries, rctx.Field.Name+"("+argsLabel(rctx.Args)+")")
|
||||||
|
} else if name, ok := path[0].(string); ok {
|
||||||
|
globals.Queries = append(globals.Queries, name)
|
||||||
|
}
|
||||||
|
globals.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := next(ctx)
|
||||||
|
|
||||||
|
var exitStatus string
|
||||||
|
if err != nil {
|
||||||
|
exitStatus = existStatusFailure
|
||||||
|
|
||||||
|
if globals, ok := ctx.Value(ctxKey).(*Globals); ok {
|
||||||
|
globals.Lock()
|
||||||
|
globals.Failed = true
|
||||||
|
globals.Unlock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exitStatus = exitStatusSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
exitStatusLabel := prometheusclient.Labels{"exitStatus": exitStatus}
|
||||||
|
timeToResolveField.With(exitStatusLabel).Observe(float64(time.Since(observerStart).Nanoseconds() / int64(time.Millisecond)))
|
||||||
|
|
||||||
|
resolverCompletedCounter.WithLabelValues(rctx.Object, rctx.Field.Name).Inc()
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestMiddleware is a request middleware that logs metrics for prometheus. It looks busy, but the
|
||||||
|
// overhead is less than 10µs.
|
||||||
|
func RequestMiddleware() graphql.RequestMiddleware {
|
||||||
|
return func(ctx context.Context, next func(ctx context.Context) []byte) []byte {
|
||||||
|
requestStartedCounter.Inc()
|
||||||
|
|
||||||
|
// Store some values in a global context.
|
||||||
|
globals := Globals{}
|
||||||
|
|
||||||
|
// Measure the time to serve the request
|
||||||
|
observerStart := time.Now()
|
||||||
|
res := next(context.WithValue(ctx, ctxKey, &globals))
|
||||||
|
observedDuration := time.Since(observerStart)
|
||||||
|
observedDurationMS := float64(observedDuration.Nanoseconds()) / float64(time.Millisecond)
|
||||||
|
|
||||||
|
reqCtx := graphql.GetRequestContext(ctx)
|
||||||
|
|
||||||
|
// exitStatus label
|
||||||
|
exitStatusLabel := prometheusclient.Labels{"exitStatus": exitStatusSuccess}
|
||||||
|
if globals.Failed {
|
||||||
|
exitStatusLabel["exitStatus"] = existStatusFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
// queries label
|
||||||
|
sort.Strings(globals.Queries)
|
||||||
|
queriesStr := strings.Join(globals.Queries, ";")
|
||||||
|
queriesLabel := prometheusclient.Labels{
|
||||||
|
"queries": queriesStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write metrics
|
||||||
|
timeToHandleRequest.With(exitStatusLabel).Observe(observedDurationMS)
|
||||||
|
requestCompletedCounter.Inc()
|
||||||
|
if len(globals.Queries) > 0 {
|
||||||
|
if globals.Failed {
|
||||||
|
requestFailureCounter.With(queriesLabel).Inc()
|
||||||
|
} else {
|
||||||
|
requestSuccessCounter.With(queriesLabel).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
timeToHandleRequestQueryMAvg.With(queriesLabel).Set(movingAvgLatency("ms:"+queriesStr, observedDurationMS))
|
||||||
|
timeToHandleRequestQueryTotal.With(queriesLabel).Add(observedDurationMS)
|
||||||
|
responseSizeMAvg.With(queriesLabel).Set(movingAvgLatency("bytes:"+queriesStr, float64(len(res))))
|
||||||
|
responseSizeTotal.With(queriesLabel).Add(float64(len(res)))
|
||||||
|
}
|
||||||
|
requestComplexityHisto.Observe(float64(reqCtx.OperationComplexity))
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func movingAvgLatency(key string, sample float64) float64 {
|
||||||
|
latencyMapMutex.Lock()
|
||||||
|
latency, ok := latencyMap[key]
|
||||||
|
if ok {
|
||||||
|
latency = (latency * 0.9) + (sample * 0.1)
|
||||||
|
latencyMap[key] = latency
|
||||||
|
} else {
|
||||||
|
latencyMap[key] = sample
|
||||||
|
latency = sample
|
||||||
|
}
|
||||||
|
latencyMapMutex.Unlock()
|
||||||
|
|
||||||
|
return latency
|
||||||
|
}
|
||||||
|
|
||||||
|
func argsLabel(args map[string]interface{}) string {
|
||||||
|
result := ""
|
||||||
|
|
||||||
|
for key, value := range args {
|
||||||
|
if len(result) > 0 {
|
||||||
|
result += ","
|
||||||
|
}
|
||||||
|
|
||||||
|
result = key
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(value)
|
||||||
|
for rv.Kind() == reflect.Ptr {
|
||||||
|
rv = rv.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rv.Kind() != reflect.Struct {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result += "{"
|
||||||
|
|
||||||
|
first := true
|
||||||
|
for i := 0; i < rv.NumField(); i++ {
|
||||||
|
rf := rv.Field(i)
|
||||||
|
if (rf.Kind() == reflect.Ptr || rf.Kind() == reflect.Interface) && rf.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rtf := rv.Type().Field(i)
|
||||||
|
|
||||||
|
// Empty limits are as good as no limit
|
||||||
|
if rtf.Name == "Limit" && rf.Type().Kind() == reflect.Int && rf.Int() <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !first {
|
||||||
|
result += ","
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
jsonTag := rtf.Tag.Get("json")
|
||||||
|
if jsonTag != "" {
|
||||||
|
result += jsonTag
|
||||||
|
} else {
|
||||||
|
result += strings.ToLower(rtf.Name[0:1]) + rtf.Name[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result += "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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) (models.Channel, error) {
|
|
||||||
if !strings.HasPrefix(key, "Channel.") {
|
|
||||||
key = "Channel." + key
|
|
||||||
}
|
|
||||||
|
|
||||||
if loader.loaders[key] == nil {
|
|
||||||
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 models.Channel{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, ok := res.(models.Channel)
|
|
||||||
if !ok {
|
|
||||||
return channel, errors.New("incorrect type")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 := channels.ListNames(names...)
|
|
||||||
if err != nil {
|
|
||||||
for range names {
|
|
||||||
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range names {
|
|
||||||
found := false
|
|
||||||
for i := range channels {
|
|
||||||
if channels[i].Name == name {
|
|
||||||
results = append(results, &dataloader.Result{Data: channels[i]})
|
|
||||||
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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) (models.Character, error) {
|
|
||||||
if !strings.HasPrefix(key, "Character.") {
|
|
||||||
key = "Character." + key
|
|
||||||
}
|
|
||||||
|
|
||||||
loader.loadPrimed(key)
|
|
||||||
|
|
||||||
if loader.loaders[key] == nil {
|
|
||||||
return models.Character{}, errors.New("unsupported key")
|
|
||||||
}
|
|
||||||
|
|
||||||
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
|
|
||||||
res, err := thunk()
|
|
||||||
if err != nil {
|
|
||||||
return models.Character{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
char, ok := res.(models.Character)
|
|
||||||
if !ok {
|
|
||||||
return models.Character{}, errors.New("incorrect type")
|
|
||||||
}
|
|
||||||
|
|
||||||
return char, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Characters gets characters by key
|
|
||||||
func (loader *Loader) Characters(key string, values ...string) ([]models.Character, error) {
|
|
||||||
if !strings.HasPrefix(key, "Character.") {
|
|
||||||
key = "Character." + key
|
|
||||||
}
|
|
||||||
|
|
||||||
if loader.loaders[key] == nil {
|
|
||||||
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 {
|
|
||||||
if err != nil && err != ErrNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chars := make([]models.Character, len(res))
|
|
||||||
|
|
||||||
for i := range res {
|
|
||||||
char, ok := res[i].(models.Character)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("incorrect type")
|
|
||||||
}
|
|
||||||
|
|
||||||
chars[i] = 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 := characters.List(&characters.Filter{IDs: ids})
|
|
||||||
if err != nil {
|
|
||||||
for range ids {
|
|
||||||
results = append(results, &dataloader.Result{Error: err})
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, id := range ids {
|
|
||||||
found := false
|
|
||||||
|
|
||||||
for _, character := range characters {
|
|
||||||
if character.ID == id {
|
|
||||||
results = append(results, &dataloader.Result{Data: character})
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
results = append(results, &dataloader.Result{Data: models.Character{}, Error: ErrNotFound})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
|
||||||
var results []*dataloader.Result
|
|
||||||
nicks := keys.Keys()
|
|
||||||
|
|
||||||
characters, err := characters.List(&characters.Filter{Nicks: nicks})
|
|
||||||
if err != nil {
|
|
||||||
for range nicks {
|
|
||||||
results = append(results, &dataloader.Result{Error: err})
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, nick := range nicks {
|
|
||||||
found := false
|
|
||||||
for i := range characters {
|
|
||||||
if characters[i].HasNick(nick) {
|
|
||||||
results = append(results, &dataloader.Result{Data: characters[i]})
|
|
||||||
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
results = append(results, &dataloader.Result{Data: models.Character{}, Error: err})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/graph-gophers/dataloader"
|
|
||||||
)
|
|
||||||
|
|
||||||
var contextKey struct{}
|
|
||||||
|
|
||||||
// ErrNotFound is returned in batches when one or more things weren't found. Usually harmless.
|
|
||||||
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.
|
|
||||||
func New() *Loader {
|
|
||||||
return &Loader{
|
|
||||||
ctx: context.Background(),
|
|
||||||
loaders: map[string]*dataloader.Loader{
|
|
||||||
"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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromContext gets the Loader from context.
|
|
||||||
func FromContext(ctx context.Context) *Loader {
|
|
||||||
value := ctx.Value(&contextKey)
|
|
||||||
if value == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.(*Loader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToContext gets a context with the loader as a value
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Notifier is a synchronization primitive for waking upp all listeners at once.
|
||||||
|
type Notifier struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
ch chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast wakes all listeners if there are any.
|
||||||
|
func (notifier *Notifier) Broadcast() {
|
||||||
|
notifier.mutex.Lock()
|
||||||
|
if notifier.ch != nil {
|
||||||
|
close(notifier.ch)
|
||||||
|
notifier.ch = nil
|
||||||
|
}
|
||||||
|
notifier.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// C gets the channel that'll close on the next notification.
|
||||||
|
func (notifier *Notifier) C() <-chan struct{} {
|
||||||
|
notifier.mutex.Lock()
|
||||||
|
if notifier.ch == nil {
|
||||||
|
notifier.ch = make(chan struct{})
|
||||||
|
}
|
||||||
|
ch := notifier.ch
|
||||||
|
notifier.mutex.Unlock()
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the next `Broadcast` call, or the context's termination.
|
||||||
|
func (notifier *Notifier) Wait(ctx context.Context) error {
|
||||||
|
select {
|
||||||
|
case <-notifier.C():
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var db *mgo.Database
|
|
||||||
var dbInits []func(db *mgo.Database)
|
|
||||||
|
|
||||||
// ConnectDB connects to a mongodb database.
|
|
||||||
func ConnectDB(host string, port int, database, username, password, mechanism string) error {
|
|
||||||
session, err := mgo.DialWithInfo(&mgo.DialInfo{
|
|
||||||
Addrs: []string{fmt.Sprintf("%s:%d", host, port)},
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
Database: database,
|
|
||||||
Username: username,
|
|
||||||
Password: password,
|
|
||||||
Mechanism: mechanism,
|
|
||||||
Source: database,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
db = session.DB(database)
|
|
||||||
|
|
||||||
return setupDB()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleInit handles the initialization of the database
|
|
||||||
func HandleInit(function func(db *mgo.Database)) {
|
|
||||||
dbInits = append(dbInits, function)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupDB() error {
|
|
||||||
db.C("common.characters").EnsureIndexKey("name")
|
|
||||||
db.C("common.characters").EnsureIndexKey("shortName")
|
|
||||||
db.C("common.characters").EnsureIndexKey("author")
|
|
||||||
err := db.C("common.characters").EnsureIndex(mgo.Index{
|
|
||||||
Key: []string{"nicks"},
|
|
||||||
Unique: true,
|
|
||||||
DropDups: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
db.C("logbot3.logs").EnsureIndexKey("date")
|
|
||||||
db.C("logbot3.logs").EnsureIndexKey("channel")
|
|
||||||
db.C("logbot3.logs").EnsureIndexKey("channel", "open")
|
|
||||||
db.C("logbot3.logs").EnsureIndexKey("open")
|
|
||||||
db.C("logbot3.logs").EnsureIndexKey("oldId")
|
|
||||||
db.C("logbot3.logs").EnsureIndexKey("characterIds")
|
|
||||||
db.C("logbot3.logs").EnsureIndexKey("event")
|
|
||||||
db.C("logbot3.logs").EnsureIndexKey("$text:channel", "$text:title", "$text:event", "$text:description", "$text:posts.nick", "$text:posts.text")
|
|
||||||
|
|
||||||
err = db.C("server.changes").EnsureIndex(mgo.Index{
|
|
||||||
Key: []string{"date"},
|
|
||||||
ExpireAfter: time.Hour * (24 * 14),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dbInit := range dbInits {
|
|
||||||
dbInit(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
var initMuted sync.Mutex
|
|
||||||
var hasInitialized bool
|
|
||||||
|
|
||||||
// Init initalizes the store
|
|
||||||
func Init() error {
|
|
||||||
initMuted.Lock()
|
|
||||||
defer initMuted.Unlock()
|
|
||||||
if hasInitialized {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
conf := config.Global()
|
|
||||||
|
|
||||||
dbconf := conf.Database
|
|
||||||
err := ConnectDB(dbconf.Host, dbconf.Port, dbconf.Db, dbconf.Username, dbconf.Password, dbconf.Mechanism)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sconf := conf.Space
|
|
||||||
err = ConnectSpace(sconf.Host, sconf.AccessKey, sconf.SecretKey, sconf.Bucket, sconf.MaxSize, sconf.Root)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
hasInitialized = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
minio "github.com/minio/minio-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
var spaceBucket string
|
|
||||||
var spaceURLRoot string
|
|
||||||
var spaceRoot string
|
|
||||||
var spaceClient *minio.Client
|
|
||||||
var spaceMaxSize int64
|
|
||||||
|
|
||||||
// ConnectSpace connects to a S3 space.
|
|
||||||
func ConnectSpace(host, accessKey, secretKey, bucket string, maxSize int64, rootDirectory string) error {
|
|
||||||
client, err := minio.New(host, accessKey, secretKey, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
exists, err := client.BucketExists(bucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return errors.New("Bucket not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
spaceClient = client
|
|
||||||
spaceBucket = bucket
|
|
||||||
spaceURLRoot = fmt.Sprintf("https://%s.%s/%s/", bucket, host, rootDirectory)
|
|
||||||
spaceMaxSize = maxSize
|
|
||||||
spaceRoot = rootDirectory
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadFile uploads the file to the space. This does not do any checks on it, so the endpoints should
|
|
||||||
// ensure that's all okay.
|
|
||||||
func UploadFile(ctx context.Context, folder string, name string, mimeType string, reader io.Reader, size int64) (string, error) {
|
|
||||||
path := folder + "/" + name
|
|
||||||
|
|
||||||
if size > spaceMaxSize {
|
|
||||||
return "", errors.New("File is too big")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := spaceClient.PutObjectWithContext(ctx, spaceBucket, spaceRoot+"/"+path, reader, size, minio.PutObjectOptions{
|
|
||||||
ContentType: mimeType,
|
|
||||||
UserMetadata: map[string]string{
|
|
||||||
"x-amz-acl": "public-read",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = spaceClient.StatObject(spaceBucket, spaceRoot+"/"+path, minio.StatObjectOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveFile removes a file from the space
|
|
||||||
func RemoveFile(folder string, name string) error {
|
|
||||||
path := folder + "/" + name
|
|
||||||
|
|
||||||
return spaceClient.RemoveObject(spaceBucket, spaceRoot+"/"+path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadFile opens a file for download, using the same path format as the UploadFile function. Remember to Close it!
|
|
||||||
func DownloadFile(ctx context.Context, path string) (io.ReadCloser, error) {
|
|
||||||
return spaceClient.GetObjectWithContext(ctx, spaceBucket, spaceRoot+"/"+path, minio.GetObjectOptions{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLFromPath gets the URL from the path returned by UploadFile
|
|
||||||
func URLFromPath(path string) string {
|
|
||||||
return spaceURLRoot + path
|
|
||||||
}
|
|
||||||
@@ -24,12 +24,16 @@ const (
|
|||||||
ChangeModelTag ChangeModel = "Tag"
|
ChangeModelTag ChangeModel = "Tag"
|
||||||
// ChangeModelChapter is a value of ChangeModel
|
// ChangeModelChapter is a value of ChangeModel
|
||||||
ChangeModelChapter ChangeModel = "Chapter"
|
ChangeModelChapter ChangeModel = "Chapter"
|
||||||
|
// ChangeModelComment is a value of ChangeModel
|
||||||
|
ChangeModelComment ChangeModel = "Comment"
|
||||||
|
// ChangeModelFile is a value of ChangeModel
|
||||||
|
ChangeModelFile ChangeModel = "File"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsValid returns true if the underlying string is one of the correct values.
|
// IsValid returns true if the underlying string is one of the correct values.
|
||||||
func (e ChangeModel) IsValid() bool {
|
func (e ChangeModel) IsValid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case ChangeModelCharacter, ChangeModelChannel, ChangeModelLog, ChangeModelPost, ChangeModelStory, ChangeModelTag, ChangeModelChapter:
|
case ChangeModelCharacter, ChangeModelChannel, ChangeModelLog, ChangeModelPost, ChangeModelStory, ChangeModelTag, ChangeModelChapter, ChangeModelComment:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
+139
-13
@@ -1,6 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Change represents a change in the rpdata history through the API.
|
// Change represents a change in the rpdata history through the API.
|
||||||
type Change struct {
|
type Change struct {
|
||||||
@@ -12,24 +15,74 @@ type Change struct {
|
|||||||
Keys []ChangeKey `bson:"keys"`
|
Keys []ChangeKey `bson:"keys"`
|
||||||
Date time.Time `bson:"date"`
|
Date time.Time `bson:"date"`
|
||||||
|
|
||||||
Logs []Log `bson:"logs"`
|
Logs []*Log `bson:"logs"`
|
||||||
Characters []Character `bson:"characters"`
|
Characters []*Character `bson:"characters"`
|
||||||
Channels []Channel `bson:"channels"`
|
Channels []*Channel `bson:"channels"`
|
||||||
Posts []Post `bson:"posts"`
|
Posts []*Post `bson:"posts"`
|
||||||
Stories []Story `bson:"stories"`
|
Stories []*Story `bson:"stories"`
|
||||||
Tags []Tag `bson:"tags"`
|
Tags []*Tag `bson:"tags"`
|
||||||
Chapters []Chapter `bson:"chapters"`
|
Chapters []*Chapter `bson:"chapters"`
|
||||||
|
Comments []*Comment `bson:"comments"`
|
||||||
|
Files []*File `bson:"files"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeKey is a key for a change that can be used when subscribing to them.
|
// AddObject adds the model into the appropriate array.
|
||||||
type ChangeKey struct {
|
func (change *Change) AddObject(object interface{}) bool {
|
||||||
Model ChangeModel `bson:"model"`
|
if v := reflect.ValueOf(object); v.Kind() == reflect.Struct {
|
||||||
ID string `bson:"id"`
|
ptr := reflect.PtrTo(v.Type())
|
||||||
|
ptrValue := reflect.New(ptr.Elem())
|
||||||
|
ptrValue.Elem().Set(v)
|
||||||
|
|
||||||
|
object = ptrValue.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch object := object.(type) {
|
||||||
|
case *Log:
|
||||||
|
change.Logs = append(change.Logs, object)
|
||||||
|
case []*Log:
|
||||||
|
change.Logs = append(change.Logs, object...)
|
||||||
|
case *Character:
|
||||||
|
change.Characters = append(change.Characters, object)
|
||||||
|
case []*Character:
|
||||||
|
change.Characters = append(change.Characters, object...)
|
||||||
|
case *Channel:
|
||||||
|
change.Channels = append(change.Channels, object)
|
||||||
|
case []*Channel:
|
||||||
|
change.Channels = append(change.Channels, object...)
|
||||||
|
case *Post:
|
||||||
|
change.Posts = append(change.Posts, object)
|
||||||
|
case []*Post:
|
||||||
|
change.Posts = append(change.Posts, object...)
|
||||||
|
case *Story:
|
||||||
|
change.Stories = append(change.Stories, object)
|
||||||
|
case []*Story:
|
||||||
|
change.Stories = append(change.Stories, object...)
|
||||||
|
case *Tag:
|
||||||
|
change.Tags = append(change.Tags, object)
|
||||||
|
case []*Tag:
|
||||||
|
change.Tags = append(change.Tags, object...)
|
||||||
|
case *Chapter:
|
||||||
|
change.Chapters = append(change.Chapters, object)
|
||||||
|
case []*Chapter:
|
||||||
|
change.Chapters = append(change.Chapters, object...)
|
||||||
|
case *Comment:
|
||||||
|
change.Comments = append(change.Comments, object)
|
||||||
|
case []*Comment:
|
||||||
|
change.Comments = append(change.Comments, object...)
|
||||||
|
case *File:
|
||||||
|
change.Files = append(change.Files, object)
|
||||||
|
case []*File:
|
||||||
|
change.Files = append(change.Files, object...)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Objects makes a combined, mixed array of all the models stored in this change.
|
// Objects makes a combined, mixed array of all the models stored in this change.
|
||||||
func (change *Change) Objects() []interface{} {
|
func (change *Change) Objects() []interface{} {
|
||||||
data := make([]interface{}, 0, len(change.Logs)+len(change.Characters)+len(change.Channels)+len(change.Posts)+len(change.Stories)+len(change.Tags)+len(change.Chapters))
|
data := make([]interface{}, 0, 8)
|
||||||
|
|
||||||
for _, log := range change.Logs {
|
for _, log := range change.Logs {
|
||||||
data = append(data, log)
|
data = append(data, log)
|
||||||
@@ -52,6 +105,79 @@ func (change *Change) Objects() []interface{} {
|
|||||||
for _, chapter := range change.Chapters {
|
for _, chapter := range change.Chapters {
|
||||||
data = append(data, chapter)
|
data = append(data, chapter)
|
||||||
}
|
}
|
||||||
|
for _, comment := range change.Comments {
|
||||||
|
data = append(data, comment)
|
||||||
|
}
|
||||||
|
for _, file := range change.Files {
|
||||||
|
data = append(data, file)
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (change *Change) PassesFilter(filter ChangeFilter) bool {
|
||||||
|
if filter.Author != nil && change.Author != *filter.Author {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// For unlisted changes, pass it only if the filter refers to the specific index.
|
||||||
|
if !change.Listed {
|
||||||
|
hasSpecificKey := false
|
||||||
|
|
||||||
|
KeyFindLoop:
|
||||||
|
for _, key := range filter.Keys {
|
||||||
|
if key.ID == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, changeKey := range change.Keys {
|
||||||
|
if changeKey.Model == key.Model && changeKey.ID == key.ID {
|
||||||
|
hasSpecificKey = true
|
||||||
|
break KeyFindLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasSpecificKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.EarliestDate != nil && filter.EarliestDate.Before(change.Date) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Keys) > 0 {
|
||||||
|
foundKey := false
|
||||||
|
|
||||||
|
KeyFindLoop2:
|
||||||
|
for _, key := range filter.Keys {
|
||||||
|
for _, changeKey := range change.Keys {
|
||||||
|
if changeKey == key {
|
||||||
|
foundKey = true
|
||||||
|
break KeyFindLoop2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeKey is a key for a change that can be used when subscribing to them.
|
||||||
|
type ChangeKey struct {
|
||||||
|
Model ChangeModel `bson:"model"`
|
||||||
|
ID string `bson:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeFilter is a filter for listing changes.
|
||||||
|
type ChangeFilter struct {
|
||||||
|
Keys []ChangeKey
|
||||||
|
EarliestDate *time.Time
|
||||||
|
Author *string
|
||||||
|
Limit *int
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package changekeys
|
|||||||
|
|
||||||
import "git.aiterp.net/rpdata/api/models"
|
import "git.aiterp.net/rpdata/api/models"
|
||||||
|
|
||||||
// Listed is a helper for cases like []models.ChangeKey{changekeys.All("Logs"), changekeys.One(log)}
|
// Listed is a helper for cases like []models.ChangeKey{All("Log"), One(log), One(post)}
|
||||||
func Listed(objects ...interface{}) []models.ChangeKey {
|
func Listed(objects ...interface{}) []models.ChangeKey {
|
||||||
keys := Many(objects)
|
keys := Many(objects)
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func TestMany(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
models.Post{ID: "P5"},
|
models.Post{ID: "P5"},
|
||||||
|
models.Comment{ID: "SCC1234"},
|
||||||
}
|
}
|
||||||
|
|
||||||
expectations := []models.ChangeKey{
|
expectations := []models.ChangeKey{
|
||||||
@@ -33,6 +34,7 @@ func TestMany(t *testing.T) {
|
|||||||
{Model: "Character", ID: "C17"},
|
{Model: "Character", ID: "C17"},
|
||||||
{Model: "Channel", ID: "#Stuff"},
|
{Model: "Channel", ID: "#Stuff"},
|
||||||
{Model: "Post", ID: "P5"},
|
{Model: "Post", ID: "P5"},
|
||||||
|
{Model: "Comment", ID: "SCC1234"},
|
||||||
}
|
}
|
||||||
|
|
||||||
results := changekeys.Many(data...)
|
results := changekeys.Many(data...)
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ func One(object interface{}) models.ChangeKey {
|
|||||||
switch v := object.(type) {
|
switch v := object.(type) {
|
||||||
case models.Post:
|
case models.Post:
|
||||||
return models.ChangeKey{Model: "Post", ID: v.ID}
|
return models.ChangeKey{Model: "Post", ID: v.ID}
|
||||||
|
case *models.Post:
|
||||||
|
return models.ChangeKey{Model: "Post", ID: v.ID}
|
||||||
case models.Character:
|
case models.Character:
|
||||||
return models.ChangeKey{Model: "Character", ID: v.ID}
|
return models.ChangeKey{Model: "Character", ID: v.ID}
|
||||||
|
case *models.Character:
|
||||||
|
return models.ChangeKey{Model: "Character", ID: v.ID}
|
||||||
case models.Channel:
|
case models.Channel:
|
||||||
return models.ChangeKey{Model: "Channel", ID: v.Name}
|
return models.ChangeKey{Model: "Channel", ID: v.Name}
|
||||||
|
case *models.Channel:
|
||||||
|
return models.ChangeKey{Model: "Channel", ID: v.Name}
|
||||||
}
|
}
|
||||||
|
|
||||||
model := ""
|
model := ""
|
||||||
@@ -26,6 +32,10 @@ func One(object interface{}) models.ChangeKey {
|
|||||||
|
|
||||||
id := ""
|
id := ""
|
||||||
v := reflect.ValueOf(object)
|
v := reflect.ValueOf(object)
|
||||||
|
for v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
if f := v.FieldByName("ID"); f.Kind() == reflect.String {
|
if f := v.FieldByName("ID"); f.Kind() == reflect.String {
|
||||||
id = f.String()
|
id = f.String()
|
||||||
} else if f = v.FieldByName("Name"); f.Kind() == reflect.String {
|
} else if f = v.FieldByName("Name"); f.Kind() == reflect.String {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ func TestOne(t *testing.T) {
|
|||||||
models.Log{ID: "2018-10-23_210303325_RedrockAgency", ShortID: "L807"},
|
models.Log{ID: "2018-10-23_210303325_RedrockAgency", ShortID: "L807"},
|
||||||
models.ChangeKey{Model: "Log", ID: "2018-10-23_210303325_RedrockAgency"},
|
models.ChangeKey{Model: "Log", ID: "2018-10-23_210303325_RedrockAgency"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Comment_SCC1234",
|
||||||
|
models.Comment{ID: "SCC1234"},
|
||||||
|
models.ChangeKey{Model: "Comment", ID: "SCC1234"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, row := range table {
|
for _, row := range table {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package changes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var collection *mgo.Collection
|
|
||||||
var submitMutex sync.Mutex
|
|
||||||
|
|
||||||
func list(query bson.M, limit int) ([]models.Change, error) {
|
|
||||||
changes := make([]models.Change, 0, 64)
|
|
||||||
err := collection.Find(query).Limit(limit).Sort("date").All(&changes)
|
|
||||||
|
|
||||||
return changes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
collection = db.C("common.changes")
|
|
||||||
|
|
||||||
collection.EnsureIndexKey("date")
|
|
||||||
collection.EnsureIndexKey("author")
|
|
||||||
collection.EnsureIndexKey("keys")
|
|
||||||
|
|
||||||
err := collection.EnsureIndex(mgo.Index{
|
|
||||||
Name: "expiry",
|
|
||||||
Key: []string{"date"},
|
|
||||||
ExpireAfter: time.Hour * 2400, // 100 days
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user