Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b7b8f1b6 | ||
|
|
e9b00b61fd | ||
|
|
c9084287a4 | ||
|
|
570d0e2bd2 | ||
|
|
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 | ||
|
|
d038c1cfea | ||
|
|
7e812bd615 | ||
|
|
947765884e | ||
|
|
ffec008820 | ||
|
|
138b3e3eab | ||
|
|
929abfdbe4 | ||
|
|
a8cd595876 | ||
|
|
9ed3b32b42 | ||
|
|
6bae52d652 | ||
|
|
cee370c2ac | ||
|
|
9affdbd20e | ||
|
|
cf545cde3b | ||
|
|
b414cf118f | ||
|
|
4979b02104 | ||
|
|
b7d5059d60 | ||
|
|
9fbdc767ad | ||
|
|
daeaaf5fa9 | ||
|
|
d3277c7476 | ||
|
|
90681c67b0 | ||
|
|
bebd99833b | ||
|
|
34f86d5037 | ||
|
|
8a85313762 | ||
|
|
017e68eae7 | ||
|
|
1955ad0517 | ||
|
|
34ba5d5d3a | ||
|
|
8c8a251080 | ||
|
|
73fe125870 | ||
|
|
04ec85aadf | ||
|
|
59233ee549 | ||
|
|
7aa0de9135 | ||
|
|
6dfbee7ff2 | ||
|
|
8a07c2ffdb | ||
|
|
0e9536b0a8 | ||
|
|
b8e5ed5813 | ||
|
|
2317f66920 | ||
|
|
fc488b938d | ||
|
|
ceb2617571 | ||
|
|
aeb5b26627 | ||
|
|
6aafc67790 | ||
|
|
c6f8fd9faa | ||
|
|
efa98b80cc | ||
|
|
707dcb43eb | ||
|
|
9bcc28a0ea | ||
|
|
2fceaa62ed | ||
|
|
86125c7ce9 | ||
|
|
bb28795cd5 | ||
|
|
815f30597e | ||
|
|
cd1f122502 | ||
|
|
0e0d802a32 | ||
|
|
3c42043ab6 | ||
|
|
8388ee2446 | ||
|
|
260c2e7f76 | ||
|
|
a4034b0faa | ||
|
|
c3a6798050 | ||
|
|
2ab933149e | ||
|
|
07b94b3cab | ||
|
|
79171bf9a2 | ||
|
|
bac57bc378 | ||
|
|
875dac9ecb | ||
|
|
0768cfd0bd | ||
|
|
2b05d90f4a | ||
|
|
0444911e73 | ||
|
|
1be4088a8f | ||
|
|
9978d7380b | ||
|
|
2e5162a29b | ||
|
|
dac0060155 | ||
|
|
9d7fd498a9 | ||
|
|
e0af2685ee | ||
|
|
3184b60310 | ||
|
|
675c09ef04 | ||
|
|
9707320960 | ||
|
|
20e53b4f5e | ||
|
|
8bb4dea358 | ||
|
|
75b2382f09 | ||
|
|
1ca25eff8e | ||
|
|
bf10ed1fe3 | ||
|
|
54136be912 | ||
|
|
228c2bfeb4 | ||
|
|
2f98be5747 | ||
|
|
2a40f97282 | ||
|
|
e5b4184106 | ||
|
|
c348a90b70 | ||
|
|
132cf28f0f | ||
|
|
d5df624188 | ||
|
|
ca98648274 | ||
|
|
4642a8abec | ||
|
|
0ec923d686 | ||
|
|
388d5378e7 | ||
|
|
67e9e8f1e6 | ||
|
|
06bc7e5a1b | ||
|
|
237190e498 | ||
|
|
b9f0fef966 | ||
|
|
ba1c1e0a4b | ||
|
|
18e77b8205 | ||
|
|
d8871259a1 | ||
|
|
26d7e36631 | ||
|
|
51489ed901 | ||
|
|
ed3e384d55 | ||
|
|
9455fd960f | ||
|
|
36939ffc59 | ||
|
|
5fedce5899 | ||
|
|
dced5282b1 | ||
|
|
d05f3a8ca8 | ||
|
|
53e2dfc6b6 | ||
|
|
fba499024b | ||
|
|
cc765e220a | ||
|
|
ba49b1fd30 | ||
|
|
8c290c8dcd | ||
|
|
576e1df001 | ||
|
|
7adcdfe1e0 | ||
|
|
6f04da49b8 | ||
|
|
9b705e6514 | ||
|
|
155309693a | ||
|
|
5895e09f37 | ||
|
|
223d9922e9 | ||
|
|
429387c9ac | ||
|
|
a7113d5efe | ||
|
|
a0cb8d7fa8 | ||
|
|
5189d486a5 | ||
|
|
011cb00001 | ||
|
|
7a52504381 | ||
|
|
866a5056be | ||
|
|
e0409a6635 | ||
|
|
bae15de3e8 | ||
|
|
3f5af44dd6 | ||
|
|
4bbb8bb9ae | ||
|
|
87cd5e8e3a | ||
|
|
136302084d | ||
|
|
0122f64fbb | ||
|
|
6bfb9cbe20 | ||
|
|
fca5176579 | ||
|
|
c4f7846269 |
+34
-16
@@ -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:
|
||||||
tags: latest
|
auto_tag: true
|
||||||
auto_tag: true
|
username:
|
||||||
secrets: [ docker_username, docker_password ]
|
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-dev:
|
|
||||||
|
- name: docker-release-latest
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
repo: vmaple/rpdata
|
settings:
|
||||||
tags: trunk
|
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: push
|
event:
|
||||||
|
- tag
|
||||||
+16
-2
@@ -1,6 +1,20 @@
|
|||||||
vendor
|
vendor
|
||||||
schema/bindata.go
|
graphql/schema/bindata.go
|
||||||
config.json
|
config.json
|
||||||
api
|
api
|
||||||
debug
|
debug
|
||||||
build
|
build
|
||||||
|
rpdata-graphiql
|
||||||
|
rpdata-*
|
||||||
|
!rpdata-*/
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
|
/main
|
||||||
|
/main.exe
|
||||||
|
|
||||||
|
/graph2/graphcore/*_gen.go
|
||||||
|
generated.gql
|
||||||
|
generated.go
|
||||||
|
/dump.zip
|
||||||
|
/rpdata-dump-v1/
|
||||||
|
|||||||
+20
-18
@@ -1,27 +1,29 @@
|
|||||||
# Use Go 1.10
|
## 1. Build Server
|
||||||
FROM golang:1.10
|
# Use Go 1.14
|
||||||
|
FROM golang:1.16 AS build-server
|
||||||
|
|
||||||
# Load repository into docker
|
# Load repository into docker
|
||||||
WORKDIR /go/src/git.aiterp.net/rpdata/api/
|
WORKDIR /project/
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 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/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-graphiql ./cmd/rpdata-graphiql
|
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 -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-dump ./cmd/rpdata-dump
|
||||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2logimport ./cmd/rpdata-lb2logimport
|
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-restore ./cmd/rpdata-restore
|
||||||
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
|
|
||||||
|
|
||||||
# Move artifacts into final container
|
## 2. Distribute
|
||||||
FROM alpine:latest
|
# Use alpine linux
|
||||||
RUN apk add --no-cache ca-certificates
|
FROM alpine:3.13
|
||||||
COPY --from=0 /binaries/* /usr/local/bin/
|
|
||||||
CMD ["/usr/local/bin/rpdata-graphiql"]
|
# Install certificates to enable HTTPS GET requests, and tzdata for log imports.
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
# Copy build files into final container
|
||||||
|
COPY --from=build-server /binaries/* /usr/local/bin/
|
||||||
|
|
||||||
|
# The server is the main entry point
|
||||||
|
CMD ["/usr/local/bin/rpdata-server"]
|
||||||
|
|||||||
Generated
-178
@@ -1,178 +0,0 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "git.aiterp.net/aiterp/wikiauth"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "33860de804ddcec4fc17998745233b4ec477e7c2"
|
|
||||||
|
|
||||||
[[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/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]]
|
|
||||||
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/sadbox/mediawiki"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "39fea8a1336076a961a300d1d95765dcd17e8a3c"
|
|
||||||
version = "v0.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/sirupsen/logrus"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
|
||||||
version = "v1.0.5"
|
|
||||||
|
|
||||||
[[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]]
|
|
||||||
name = "google.golang.org/appengine"
|
|
||||||
packages = ["cloudsql"]
|
|
||||||
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[solve-meta]
|
|
||||||
analyzer-name = "dep"
|
|
||||||
analyzer-version = 1
|
|
||||||
inputs-digest = "16fd84401bc5796ffeaa895f31d099e1a23b363b15b87386877de0829ac9575b"
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
||||||
-41
@@ -1,41 +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
|
|
||||||
|
|
||||||
[[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,18 +1,5 @@
|
|||||||
# RPData API
|
# RPData API
|
||||||
|
|
||||||
Project RPData aims to centralize the AiteRP applications, and partially involve the wiki, into a common API. This will allow for easy linking and inlining between them. The primary consumers of this API will be the Logbot and the upcoming RPData website.
|
Project RPData aims to centralize the AiteRP applications, and partially involve the wiki into a common API. This will allow for easy linking and inlining between them. The primary consumers of this API will be the Logbot and the upcoming RPData website.
|
||||||
|
|
||||||
## Progress
|
There is some renovation going on, and the packages `model` and `graphql` are going to be superceded by `models` and `graph2`
|
||||||
|
|
||||||
### Complete
|
|
||||||
* Session
|
|
||||||
* Character
|
|
||||||
* Log
|
|
||||||
* Post
|
|
||||||
* File
|
|
||||||
|
|
||||||
### Remaining
|
|
||||||
* Story
|
|
||||||
|
|
||||||
### Future
|
|
||||||
* User (manual DB edits will suffice for now)
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/model/story"
|
|
||||||
_ "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([]story.Tag, 0, 8)
|
|
||||||
for _, tagResult := range tagResults {
|
|
||||||
if tagResult.PageID == result.ID {
|
|
||||||
tags = append(tags, story.Tag{Kind: tagResult.Type, Name: tagResult.Name})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
story, err := story.New(result.Name, result.Author, result.Category, false, false, tags, publishDate, fictionalDate)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
title := result.Name
|
|
||||||
if strings.HasPrefix(result.Source, "#") {
|
|
||||||
firstNewline := strings.Index(result.Source, "\n")
|
|
||||||
title = result.Source[1:firstNewline]
|
|
||||||
result.Source = result.Source[firstNewline+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter, err := story.AddChapter(title, result.Author, 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"`
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"git.aiterp.net/rpdata/api/database"
|
||||||
|
"git.aiterp.net/rpdata/api/database/mongodb"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/config"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagDriver = flag.String("driver", "mongodb", "The database driver to use.")
|
||||||
|
var flagHost = flag.String("host", "127.0.0.1", "The host to connect to.")
|
||||||
|
var flagPort = flag.Int("port", 27017, "The port to connect on.")
|
||||||
|
var flagDb = flag.String("db", "rpdata", "The database name")
|
||||||
|
var flagUsername = flag.String("username", "", "")
|
||||||
|
var flagPassword = flag.String("password", "", "")
|
||||||
|
var flagMechanism = flag.String("mechanism", "", "")
|
||||||
|
var flagOutputFile = flag.String("outfile", "dump.zip", "The file to write to.")
|
||||||
|
var flagIncludeKeys = flag.Bool("include-keys", false, "Whether to include the keys.")
|
||||||
|
var flagIncludeUsers = flag.Bool("include-users", false, "Whether to include the keys.")
|
||||||
|
var flagIncludeUnlisted = flag.Bool("include-unlisted", false, "Whether to include unlisted stuff.")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
trueValue := true
|
||||||
|
truePtr := &trueValue
|
||||||
|
|
||||||
|
cfg := config.Database{
|
||||||
|
Driver: *flagDriver,
|
||||||
|
Host: *flagHost,
|
||||||
|
Port: *flagPort,
|
||||||
|
Db: *flagDb,
|
||||||
|
Username: *flagUsername,
|
||||||
|
Password: *flagPassword,
|
||||||
|
Mechanism: *flagMechanism,
|
||||||
|
}
|
||||||
|
|
||||||
|
blacklist := make(map[string]bool)
|
||||||
|
|
||||||
|
mongodb.DisableFixes = true
|
||||||
|
|
||||||
|
db, err := database.Init(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to open database:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(*flagOutputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to open output file:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(file)
|
||||||
|
defer func() {
|
||||||
|
err = zipWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to close output file:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
log.Println("Dumping characters...")
|
||||||
|
characters, err := db.Characters().List(ctx, models.CharacterFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get characters:", err)
|
||||||
|
}
|
||||||
|
if len(characters) == 0 {
|
||||||
|
log.Println("No characters to dump.")
|
||||||
|
}
|
||||||
|
for i, character := range characters {
|
||||||
|
err := writeJsonFile(zipWriter, "character", i, character.ID, time.Now(), character)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write character", character.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Dumping channels...")
|
||||||
|
channels, err := db.Channels().List(ctx, models.ChannelFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get channels:", err)
|
||||||
|
}
|
||||||
|
if len(channels) == 0 {
|
||||||
|
log.Println("No channels to dump.")
|
||||||
|
}
|
||||||
|
for i, channel := range channels {
|
||||||
|
err := writeJsonFile(zipWriter, "channel", i, channel.Name, time.Now(), channel)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write channel", channel.Name, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Dumping changes...")
|
||||||
|
changes, err := db.Changes().List(ctx, models.ChangeFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get changes:", err)
|
||||||
|
}
|
||||||
|
if len(changes) == 0 {
|
||||||
|
log.Println("No changes to dump.")
|
||||||
|
}
|
||||||
|
for i, change := range changes {
|
||||||
|
if !*flagIncludeUnlisted && !change.Listed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writeJsonFile(zipWriter, "change", i, change.ID, change.Date, change)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write change", change.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Dumping stories...")
|
||||||
|
stories, err := db.Stories().List(ctx, models.StoryFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get stories:", err)
|
||||||
|
}
|
||||||
|
unlistedStories, err := db.Stories().List(ctx, models.StoryFilter{Unlisted: truePtr})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get unlisted stories:", err)
|
||||||
|
} else {
|
||||||
|
stories = append(stories, unlistedStories...)
|
||||||
|
}
|
||||||
|
if len(stories) == 0 {
|
||||||
|
log.Println("No stories to dump.")
|
||||||
|
}
|
||||||
|
for i, story := range stories {
|
||||||
|
if !*flagIncludeUnlisted && !story.Listed {
|
||||||
|
blacklist[story.ID] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writeJsonFile(zipWriter, "story", i, story.ID, story.CreatedDate, story)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write story", story.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Dumping chapters...")
|
||||||
|
chapters, err := db.Chapters().List(ctx, models.ChapterFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get chapters:", err)
|
||||||
|
}
|
||||||
|
if len(chapters) == 0 {
|
||||||
|
log.Println("No chapters to dump.")
|
||||||
|
}
|
||||||
|
for i, chapter := range chapters {
|
||||||
|
if !*flagIncludeUnlisted && blacklist[chapter.StoryID] {
|
||||||
|
blacklist[chapter.ID] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writeJsonFile(zipWriter, "chapter", i, chapter.ID, chapter.CreatedDate, chapter)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write chapter", chapter.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Dumping comments...")
|
||||||
|
comments, err := db.Comments().List(ctx, models.CommentFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get comments:", err)
|
||||||
|
}
|
||||||
|
if len(comments) == 0 {
|
||||||
|
log.Println("No comments to dump.")
|
||||||
|
}
|
||||||
|
for i, comment := range comments {
|
||||||
|
if !*flagIncludeUnlisted && blacklist[comment.ChapterID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writeJsonFile(zipWriter, "comment", i, comment.ID, comment.CreatedDate, comment)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write comment", comment.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Dumping posts...")
|
||||||
|
posts, err := db.Posts().List(ctx, models.PostFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get posts:", err)
|
||||||
|
}
|
||||||
|
if len(posts) == 0 {
|
||||||
|
log.Println("No posts to dump.")
|
||||||
|
}
|
||||||
|
for i, post := range posts {
|
||||||
|
err := writeJsonFile(zipWriter, "post", i, post.ID, post.Time, post)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write post", post.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Dumping logs...")
|
||||||
|
logs, err := db.Logs().List(ctx, models.LogFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get logs:", err)
|
||||||
|
}
|
||||||
|
if len(logs) == 0 {
|
||||||
|
log.Println("No logs to dump.")
|
||||||
|
}
|
||||||
|
for i, logEntry := range logs {
|
||||||
|
err := writeJsonFile(zipWriter, "log", i, logEntry.ID, logEntry.Date, logEntry)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write post", logEntry.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *flagIncludeUsers {
|
||||||
|
log.Println("Dumping users...")
|
||||||
|
users, err := db.Users().List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get users:", err)
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
log.Println("No users to dump.")
|
||||||
|
}
|
||||||
|
for i, userEntry := range users {
|
||||||
|
err := writeJsonFile(zipWriter, "user", i, userEntry.ID, time.Now(), userEntry)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write user", userEntry.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *flagIncludeKeys {
|
||||||
|
log.Println("Dumping keys...")
|
||||||
|
keys, err := db.Keys().List(ctx, models.KeyFilter{})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to get users:", err)
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
log.Println("No users to dump.")
|
||||||
|
}
|
||||||
|
for i, keyEntry := range keys {
|
||||||
|
err := writeJsonFile(zipWriter, "key", i, keyEntry.ID, time.Now(), keyEntry)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write key", keyEntry.ID, ":", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJsonFile(zw *zip.Writer, model string, idx int, id string, t time.Time, data interface{}) error {
|
||||||
|
w, err := zw.CreateHeader(&zip.FileHeader{
|
||||||
|
Name: fmt.Sprintf("rpdata_dump_v1/%s/%06d_%s.json", model, idx, id),
|
||||||
|
Modified: t,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(data)
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/model/channel"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/model/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := store.Init()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, err := log.List(32768)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
added := make(map[string]bool, 1024)
|
|
||||||
for _, log := range logs {
|
|
||||||
if added[log.Channel] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := channel.Ensure(log.Channel, false)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, log.ID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
added[log.Channel] = true
|
|
||||||
|
|
||||||
fmt.Println(log.Channel, "ensured")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
config.json
|
|
||||||
debug
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/session"
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/loader"
|
|
||||||
"git.aiterp.net/rpdata/api/model/file"
|
|
||||||
logModel "git.aiterp.net/rpdata/api/model/log"
|
|
||||||
"git.aiterp.net/rpdata/api/resolver"
|
|
||||||
"git.aiterp.net/rpdata/api/schema"
|
|
||||||
graphql "github.com/graph-gophers/graphql-go"
|
|
||||||
"github.com/graph-gophers/graphql-go/relay"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := store.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Failed to init store:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := logModel.UpdateAllCharacters()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Charcter updated stopped:", err)
|
|
||||||
}
|
|
||||||
log.Println("Updated characters on", n, "logs")
|
|
||||||
|
|
||||||
schema, err := graphql.ParseSchema(schema.String(), &resolver.RootResolver{}, graphql.MaxParallelism(48))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Failed to parse schema:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "GET" {
|
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write(page)
|
|
||||||
}))
|
|
||||||
|
|
||||||
relayHandler := &relay.Handler{Schema: schema}
|
|
||||||
http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r = session.Load(w, r)
|
|
||||||
|
|
||||||
l := loader.New()
|
|
||||||
r = r.WithContext(l.ToContext(r.Context()))
|
|
||||||
|
|
||||||
relayHandler.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r = session.Load(w, r)
|
|
||||||
sess := session.FromContext(r.Context())
|
|
||||||
user := sess.User()
|
|
||||||
if user == nil || !user.Permitted("file.upload") {
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.ParseMultipartForm(16384); err != nil {
|
|
||||||
http.Error(w, "Internal (1): "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
formFile, header, err := r.FormFile("file")
|
|
||||||
if err != nil || header == nil || formFile == nil {
|
|
||||||
http.Error(w, "No file provided", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := file.Upload(r.Context(), header.Filename, header.Header.Get("Content-Type"), user.ID, header.Size, formFile)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal (2): "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json, err := json.Marshal(file)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal (3): "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(json)
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":17000", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
var page = []byte(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.css" />
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.1.0/fetch.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.js"></script>
|
|
||||||
</head>
|
|
||||||
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
|
|
||||||
<div id="graphiql" style="height: 100vh;">Loading...</div>
|
|
||||||
<script>
|
|
||||||
function graphQLFetcher(graphQLParams) {
|
|
||||||
return fetch("/graphql", {
|
|
||||||
method: "post",
|
|
||||||
body: JSON.stringify(graphQLParams),
|
|
||||||
credentials: "include",
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.text();
|
|
||||||
}).then(function (responseBody) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(responseBody);
|
|
||||||
} catch (error) {
|
|
||||||
return responseBody;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ReactDOM.render(
|
|
||||||
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
|
|
||||||
document.getElementById("graphiql")
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`)
|
|
||||||
@@ -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/model/character"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 := character.New(info.Nicks[0], info.Name, info.ShortName, info.Author, "")
|
|
||||||
if err != nil {
|
|
||||||
log.Println(info.Nicks[0], "failed to insert:", err)
|
|
||||||
charsFailed++
|
|
||||||
|
|
||||||
char, err = character.FindNick(info.Nicks[0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Println(info.Nicks[0], "added")
|
|
||||||
charsAdded++
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, alt := range info.Nicks[1:] {
|
|
||||||
err := char.AddNick(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,108 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
logModel "git.aiterp.net/rpdata/api/model/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 := logModel.New(date, channel, title, event, "", false)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = l.NewPost(time.Now(), "annotation.info", "rpdata-lb2logimport", "This logfile is imported from aitelogs2 and may contain errors or wrong timestamps.")
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range logLines {
|
|
||||||
if line.Verb != "CHARS" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = l.NewPost(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 = l.NewPost(postTime, strings.ToLower(line.Verb), prefixReplacer.Replace(line.Args[0]), line.Text)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = l.UpdateCharacters()
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(l.ID, "completed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"git.aiterp.net/rpdata/api/database"
|
||||||
|
"git.aiterp.net/rpdata/api/database/mongodb"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/config"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagDriver = flag.String("driver", "mongodb", "The database driver to use.")
|
||||||
|
var flagHost = flag.String("host", "127.0.0.1", "The host to connect to.")
|
||||||
|
var flagPort = flag.Int("port", 27017, "The port to connect on.")
|
||||||
|
var flagDb = flag.String("db", "rpdata", "The database name")
|
||||||
|
var flagUsername = flag.String("username", "", "")
|
||||||
|
var flagPassword = flag.String("password", "", "")
|
||||||
|
var flagMechanism = flag.String("mechanism", "", "")
|
||||||
|
var flagInputFile = flag.String("infile", "dump.zip", "The file to read from.")
|
||||||
|
var flagIncludeKeys = flag.Bool("include-keys", false, "Whether to include the keys.")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cfg := config.Database{
|
||||||
|
Driver: *flagDriver,
|
||||||
|
Host: *flagHost,
|
||||||
|
Port: *flagPort,
|
||||||
|
Db: *flagDb,
|
||||||
|
Username: *flagUsername,
|
||||||
|
Password: *flagPassword,
|
||||||
|
Mechanism: *flagMechanism,
|
||||||
|
RestoreIDs: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
mongodb.DisableFixes = true
|
||||||
|
|
||||||
|
db, err := database.Init(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to open database:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg2 := cfg
|
||||||
|
cfg2.RestoreIDs = false
|
||||||
|
|
||||||
|
db.Tags()
|
||||||
|
|
||||||
|
zipReader, err := zip.OpenReader(*flagInputFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to open input file:", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = zipReader.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to close input file:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
postMap := make(map[string][]*models.Post)
|
||||||
|
|
||||||
|
for _, file := range zipReader.File {
|
||||||
|
if strings.HasSuffix(file.Name, "/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(file.Name, "/")
|
||||||
|
if len(parts) < 3 || parts[0] != "rpdata_dump_v1" {
|
||||||
|
log.Fatalln("Unrecognized file path:", file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Unrecognized file:", file.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hideList := make(map[string]bool)
|
||||||
|
|
||||||
|
switch parts[1] {
|
||||||
|
case "character":
|
||||||
|
{
|
||||||
|
character := models.Character{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&character)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse character:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Characters().Insert(ctx, character)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert character:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Character", character.Name, "inserted.")
|
||||||
|
}
|
||||||
|
case "channel":
|
||||||
|
{
|
||||||
|
channel := models.Channel{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&channel)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse channel:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Channels().Insert(ctx, channel)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert channel:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Channel", channel.Name, "inserted.")
|
||||||
|
}
|
||||||
|
case "change":
|
||||||
|
{
|
||||||
|
change := models.Change{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&change)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse character:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Changes().Insert(ctx, change)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert character:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if change.Listed {
|
||||||
|
log.Println("Change", change.ID, "inserted.")
|
||||||
|
} else {
|
||||||
|
log.Println("Unlisted change inserted.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "story":
|
||||||
|
{
|
||||||
|
story := models.Story{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&story)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse story:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Stories().Insert(ctx, story)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert story:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if story.Listed {
|
||||||
|
log.Println("Story", story.Name, "inserted.")
|
||||||
|
} else {
|
||||||
|
log.Println("Unlisted story inserted.")
|
||||||
|
hideList[story.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "chapter":
|
||||||
|
{
|
||||||
|
chapter := models.Chapter{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&chapter)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse story:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Chapters().Insert(ctx, chapter)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert story:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hideList[chapter.StoryID] {
|
||||||
|
log.Println("Chapter", fmt.Sprintf("%s (id: %s)", chapter.Title, chapter.ID), "inserted.")
|
||||||
|
} else {
|
||||||
|
log.Println("Unlisted chapter inserted.")
|
||||||
|
hideList[chapter.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "comment":
|
||||||
|
{
|
||||||
|
comment := models.Comment{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&comment)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse story:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Comments().Insert(ctx, comment)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert story:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hideList[comment.ChapterID] {
|
||||||
|
log.Println("Comment", comment.Subject, "inserted.")
|
||||||
|
} else {
|
||||||
|
log.Println("Unlisted comment inserted.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "log":
|
||||||
|
{
|
||||||
|
logg := models.Log{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&logg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse log:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Logs().Insert(ctx, logg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert log:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Log", logg.Date.Format(time.RFC3339)[:16], logg.ChannelName, "inserted.")
|
||||||
|
}
|
||||||
|
case "post":
|
||||||
|
{
|
||||||
|
post := models.Post{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&post)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse post:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
postMap[post.LogID] = append(postMap[post.LogID], &post)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "user":
|
||||||
|
{
|
||||||
|
user := models.User{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse post:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Users().Insert(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert user:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("User", user.ID, "inserted.")
|
||||||
|
}
|
||||||
|
case "key":
|
||||||
|
{
|
||||||
|
if !*flagIncludeKeys {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
key := models.Key{}
|
||||||
|
err := json.NewDecoder(reader).Decode(&key)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not parse key:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Keys().Insert(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert key:", parts[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Key", key.ID, "inserted.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, posts := range postMap {
|
||||||
|
_, err = db.Posts().InsertMany(ctx, posts...)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Could not insert post for logId:", posts[0].LogID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Inserted %d posts for log %s.", len(posts), posts[0].LogID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"git.aiterp.net/rpdata/api/space"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/database"
|
||||||
|
"git.aiterp.net/rpdata/api/graph2"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/config"
|
||||||
|
"git.aiterp.net/rpdata/api/internal/instrumentation"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
"github.com/99designs/gqlgen/handler"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Global()
|
||||||
|
|
||||||
|
db, err := database.Init(cfg.Database)
|
||||||
|
if err != nil {
|
||||||
|
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("/graphql", queryHandler(serviceBundle))
|
||||||
|
|
||||||
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := serviceBundle.Logs.RefreshAllLogCharacters(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Refresh Characters:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
graph2.New(services),
|
||||||
|
handler.UploadMaxSize(10485760),
|
||||||
|
handler.UploadMaxMemory(1048576),
|
||||||
|
handler.RecoverFunc(func(ctx context.Context, err interface{}) error {
|
||||||
|
log.Println(err)
|
||||||
|
log.Println(string(debug.Stack()))
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// >_>
|
||||||
|
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer of the curse") {
|
||||||
|
w.Header().Set("X-Emerald-Herald", "Seek souls. Larger, more powerful souls.")
|
||||||
|
w.Header().Add("X-Emerald-Herald", "Seek the king, that is the only way.")
|
||||||
|
w.Header().Add("X-Emerald-Herald", "Lest this land swallow you whole... As it has so many others.")
|
||||||
|
}
|
||||||
|
|
||||||
|
r = services.Auth.RequestWithToken(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/model/file"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 := file.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 := file.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,127 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type changeRepository struct {
|
||||||
|
restoreIDs bool
|
||||||
|
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 if !r.restoreIDs {
|
||||||
|
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) {
|
||||||
|
if !r.restoreIDs || change.ID == "" {
|
||||||
|
next, err := r.idCounter.Increment(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
change.ID = "Change_" + strconv.Itoa(next)
|
||||||
|
} else {
|
||||||
|
tokens := strings.Split(change.ID, "_")
|
||||||
|
if len(tokens) != 2 || tokens[0] != "Change" {
|
||||||
|
return nil, errors.New("Invalid change ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := strconv.Atoi(tokens[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.idCounter.Bump(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, restoreIDs bool) (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{
|
||||||
|
restoreIDs: restoreIDs,
|
||||||
|
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,147 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chapterRepository struct {
|
||||||
|
restoreIDs bool
|
||||||
|
chapters *mgo.Collection
|
||||||
|
comments *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChapterRepository(db *mgo.Database, restoreIDs bool) (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{
|
||||||
|
restoreIDs: restoreIDs,
|
||||||
|
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) {
|
||||||
|
if !r.restoreIDs {
|
||||||
|
chapter.ID = generate.ChapterID()
|
||||||
|
} else {
|
||||||
|
if len(chapter.ID) != len(generate.ChapterID()) && strings.HasPrefix(chapter.ID, "S") {
|
||||||
|
return nil, errors.New("invalid story id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,262 @@
|
|||||||
|
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
|
||||||
|
restoreIDs bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCharacterRepository(db *mgo.Database, restoreIDs bool) (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{
|
||||||
|
restoreIDs: restoreIDs,
|
||||||
|
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) {
|
||||||
|
if !r.restoreIDs {
|
||||||
|
nextId, err := r.cidCounter.Increment(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
character.ID = "C" + strconv.Itoa(nextId)
|
||||||
|
} else {
|
||||||
|
n, err := strconv.Atoi(character.ID[1:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.cidCounter.Bump(n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,144 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commentRepository struct {
|
||||||
|
restoreIDs bool
|
||||||
|
comments *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommentRepository(db *mgo.Database, restoreIDs bool) (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{
|
||||||
|
restoreIDs: restoreIDs,
|
||||||
|
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) {
|
||||||
|
if !r.restoreIDs {
|
||||||
|
comment.ID = generate.CommentID()
|
||||||
|
} else {
|
||||||
|
if len(comment.ID) != len(generate.CommentID()) && strings.HasPrefix(comment.ID, "SSC") {
|
||||||
|
return nil, errors.New("invalid story id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,276 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DisableFixes bool
|
||||||
|
|
||||||
|
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, cfg.RestoreIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channels, err := newChannelRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
changes, err := newChangeRepository(db, cfg.RestoreIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := newLogRepository(db, cfg.RestoreIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := newPostRepository(db, cfg.RestoreIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := newFileRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stories, err := newStoryRepository(db, cfg.RestoreIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters, err := newChapterRepository(db, cfg.RestoreIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, err := newCommentRepository(db, cfg.RestoreIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err := newKeyRepository(db, cfg.RestoreIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := newUserRepository(db)
|
||||||
|
if err != nil {
|
||||||
|
session.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !DisableFixes {
|
||||||
|
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) Bump(amount int) error {
|
||||||
|
id := c.category + "." + c.name
|
||||||
|
|
||||||
|
err := c.coll.Update(bson.M{
|
||||||
|
"_id": id,
|
||||||
|
"value": bson.M{"$lt": amount},
|
||||||
|
}, bson.M{
|
||||||
|
"value": amount,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return c.coll.Insert(bson.M{
|
||||||
|
"_id": id,
|
||||||
|
"value": amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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,75 @@
|
|||||||
|
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 {
|
||||||
|
restoreIDs bool
|
||||||
|
keys *mgo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func newKeyRepository(db *mgo.Database, restoreIDs bool) (repositories.KeyRepository, error) {
|
||||||
|
collection := db.C("auth.keys")
|
||||||
|
|
||||||
|
err := collection.EnsureIndexKey("user")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keyRepository{
|
||||||
|
keys: collection,
|
||||||
|
restoreIDs: restoreIDs,
|
||||||
|
}, 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) {
|
||||||
|
if !r.restoreIDs {
|
||||||
|
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,215 @@
|
|||||||
|
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
|
||||||
|
restoreIds bool
|
||||||
|
|
||||||
|
logs *mgo.Collection
|
||||||
|
posts *mgo.Collection
|
||||||
|
shortIdCounter *counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogRepository(db *mgo.Database, restoreIds bool) (*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{
|
||||||
|
restoreIds: restoreIds,
|
||||||
|
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) {
|
||||||
|
if !r.restoreIds || log.ID == "" || log.ShortID == "" {
|
||||||
|
nextShortId, err := r.shortIdCounter.Increment(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.ID = generate.LogID(log)
|
||||||
|
log.ShortID = "L" + strconv.Itoa(nextShortId)
|
||||||
|
} else {
|
||||||
|
n, err := strconv.Atoi(log.ShortID[1:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.shortIdCounter.Bump(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,362 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"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 {
|
||||||
|
restoreIDs bool
|
||||||
|
logs *mgo.Collection
|
||||||
|
posts *mgo.Collection
|
||||||
|
|
||||||
|
orderMutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPostRepository(db *mgo.Database, restoreIDs bool) (*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{
|
||||||
|
restoreIDs: restoreIDs,
|
||||||
|
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)
|
||||||
|
var err error
|
||||||
|
if filter.LogID != nil {
|
||||||
|
err = r.posts.Find(query).Sort("position").Limit(filter.Limit).All(&posts)
|
||||||
|
} else {
|
||||||
|
err = r.posts.Find(query).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
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.restoreIDs {
|
||||||
|
post.ID = generate.PostID()
|
||||||
|
} else {
|
||||||
|
if len(post.ID) != len(generate.PostID()) && strings.HasPrefix(post.ID, "P") {
|
||||||
|
return nil, errors.New("invalid story id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.restoreIDs || post.ID == "" {
|
||||||
|
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,224 @@
|
|||||||
|
package mongodb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type storyRepository struct {
|
||||||
|
stories *mgo.Collection
|
||||||
|
chapters *mgo.Collection
|
||||||
|
comments *mgo.Collection
|
||||||
|
restoreIDs bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStoryRepository(db *mgo.Database, restoreIDs bool) (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,
|
||||||
|
restoreIDs: restoreIDs,
|
||||||
|
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) {
|
||||||
|
if !r.restoreIDs {
|
||||||
|
story.ID = generate.StoryID()
|
||||||
|
} else {
|
||||||
|
if len(story.ID) != len(generate.StoryID()) && strings.HasPrefix(story.ID, "S") {
|
||||||
|
return nil, errors.New("invalid story id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,52 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) List(ctx context.Context) ([]*models.User, error) {
|
||||||
|
users := make([]*models.User, 0, 8)
|
||||||
|
err := r.users.Find(bson.M{}).All(&users)
|
||||||
|
if err != nil && err != mgo.ErrNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, 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,38 @@
|
|||||||
|
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/golang/snappy v0.0.2 // 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
|
||||||
|
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect
|
||||||
|
github.com/xdg/stringprep v1.0.0 // indirect
|
||||||
|
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,148 @@
|
|||||||
|
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/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
|
||||||
|
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
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=
|
||||||
|
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
|
||||||
|
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||||
|
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
|
||||||
|
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||||
|
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=
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
schema:
|
||||||
|
- schema/root.gql
|
||||||
|
- schema/types/*.gql
|
||||||
|
|
||||||
|
exec:
|
||||||
|
filename: graphcore/exec_gen.go
|
||||||
|
package: graphcore
|
||||||
|
|
||||||
|
model:
|
||||||
|
filename: graphcore/input_gen.go
|
||||||
|
package: graphcore
|
||||||
|
|
||||||
|
models:
|
||||||
|
Tag:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Tag
|
||||||
|
TagInput: #It's the same as tags
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Tag
|
||||||
|
TagKind:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.TagKind
|
||||||
|
Character:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Character
|
||||||
|
CharactersFilter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.CharacterFilter
|
||||||
|
Channel:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Channel
|
||||||
|
ChannelsFilter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChannelFilter
|
||||||
|
UnknownNick:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.UnknownNick
|
||||||
|
Post:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Post
|
||||||
|
PostsFilter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.PostFilter
|
||||||
|
Log:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Log
|
||||||
|
LogsFilter:
|
||||||
|
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:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Chapter
|
||||||
|
Story:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Story
|
||||||
|
StoryCategory:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.StoryCategory
|
||||||
|
StoriesFilter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.StoryFilter
|
||||||
|
File:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.File
|
||||||
|
FilesFilter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.FileFilter
|
||||||
|
Change:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Change
|
||||||
|
ChangeModel:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChangeModel
|
||||||
|
ChangeKey:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
||||||
|
ChangeKeyInput: # It's the same as ChangeKey
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
||||||
|
ChangesFilter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.ChangeFilter
|
||||||
|
Token:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.Token
|
||||||
|
User:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.User
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package graph2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/services"
|
||||||
|
graphql "github.com/99designs/gqlgen/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go run github.com/99designs/gqlgen -v
|
||||||
|
|
||||||
|
// New creates a new GraphQL schema.
|
||||||
|
func New(s *services.Bundle) graphql.ExecutableSchema {
|
||||||
|
return graphcore.NewExecutableSchema(graphcore.Config{
|
||||||
|
Resolvers: &rootResolver{s},
|
||||||
|
Complexity: complexity(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type rootResolver struct {
|
||||||
|
s *services.Bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Query() graphcore.QueryResolver {
|
||||||
|
return resolvers.QueryResolver(r.s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Mutation() graphcore.MutationResolver {
|
||||||
|
return resolvers.MutationResolver(r.s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Subscription() graphcore.SubscriptionResolver {
|
||||||
|
return resolvers.SubscriptionResolver(r.s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Log() graphcore.LogResolver {
|
||||||
|
return types.LogResolver(r.s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Comment() graphcore.CommentResolver {
|
||||||
|
return types.CommentResolver(r.s)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rootResolver) Token() graphcore.TokenResolver {
|
||||||
|
return types.TokenResolver(r.s)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
schema {
|
||||||
|
query: Query
|
||||||
|
mutation: Mutation
|
||||||
|
subscription: Subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
# Find character by either an ID or a nick.
|
||||||
|
character(id: String, nick: String): Character!
|
||||||
|
|
||||||
|
# Find characters
|
||||||
|
characters(filter: CharactersFilter): [Character!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Find channel by name
|
||||||
|
channel(name: String!): Channel!
|
||||||
|
|
||||||
|
# Find channels
|
||||||
|
channels(filter: ChannelsFilter): [Channel!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Find post by ID.
|
||||||
|
post(id: String!): Post!
|
||||||
|
|
||||||
|
# Find posts
|
||||||
|
posts(filter: PostsFilter): [Post!]!
|
||||||
|
|
||||||
|
|
||||||
|
"List unknown nicks"
|
||||||
|
unknownNicks(filter: UnknownNicksFilter): [UnknownNick!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Find log by ID
|
||||||
|
log(id: String!): Log!
|
||||||
|
|
||||||
|
# Find logs
|
||||||
|
logs(filter: LogsFilter): [Log!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Find story chapter by ID
|
||||||
|
chapter(id: String!): Chapter!
|
||||||
|
|
||||||
|
|
||||||
|
# Find comment by ID
|
||||||
|
comment(id: String!): Comment!
|
||||||
|
|
||||||
|
|
||||||
|
# Find all distinct tags used in stories
|
||||||
|
tags(filter: TagFilter): [Tag!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Find story by ID
|
||||||
|
story(id: String!): Story!
|
||||||
|
|
||||||
|
# Find stories
|
||||||
|
stories(filter: StoriesFilter): [Story!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Find file by ID
|
||||||
|
file(id: String!): File!
|
||||||
|
|
||||||
|
# Find files
|
||||||
|
files(filter: FilesFilter): [File!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Find changes
|
||||||
|
changes(filter: ChangesFilter): [Change!]!
|
||||||
|
|
||||||
|
|
||||||
|
# Get information about the token, useful for debugging.
|
||||||
|
token: Token!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
# Add a story
|
||||||
|
addStory(input: StoryAddInput!): Story!
|
||||||
|
|
||||||
|
# Add a story tag
|
||||||
|
addStoryTag(input: StoryTagAddInput!): Story!
|
||||||
|
|
||||||
|
# Remove a story tag
|
||||||
|
removeStoryTag(input: StoryTagRemoveInput!): Story!
|
||||||
|
|
||||||
|
# Edit a story
|
||||||
|
editStory(input: StoryEditInput!): Story!
|
||||||
|
|
||||||
|
# Remove a story
|
||||||
|
removeStory(input: StoryRemoveInput!): Story!
|
||||||
|
|
||||||
|
|
||||||
|
# Add a chapter to a story
|
||||||
|
addChapter(input: ChapterAddInput!): Chapter!
|
||||||
|
|
||||||
|
# Edit a chapter
|
||||||
|
editChapter(input: ChapterEditInput!): Chapter!
|
||||||
|
|
||||||
|
# Move a chapter
|
||||||
|
moveChapter(input: ChapterMoveInput!): Chapter!
|
||||||
|
|
||||||
|
# Remove a 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
|
||||||
|
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
|
||||||
|
editLog(input: LogEditInput!): Log!
|
||||||
|
|
||||||
|
# Remove a log
|
||||||
|
removeLog(input: LogRemoveInput!): Log!
|
||||||
|
|
||||||
|
|
||||||
|
# Add a post
|
||||||
|
addPost(input: PostAddInput!): Post!
|
||||||
|
|
||||||
|
# Edit a post
|
||||||
|
editPost(input: PostEditInput!): Post!
|
||||||
|
|
||||||
|
# Move a post. All affected posts will be returned.
|
||||||
|
movePost(input: PostMoveInput!): [Post!]!
|
||||||
|
|
||||||
|
# Remove a post
|
||||||
|
removePost(input: PostRemoveInput!): Post!
|
||||||
|
|
||||||
|
|
||||||
|
# Add a new character
|
||||||
|
addCharacter(input: CharacterAddInput!): Character!
|
||||||
|
|
||||||
|
# Add nick to character
|
||||||
|
addCharacterNick(input: CharacterNickInput!): Character!
|
||||||
|
|
||||||
|
# Remove nick from character
|
||||||
|
removeCharacterNick(input: CharacterNickInput!): Character!
|
||||||
|
|
||||||
|
# Edit character
|
||||||
|
editCharacter(input: CharacterEditInput!): Character!
|
||||||
|
|
||||||
|
# Remove a character
|
||||||
|
removeCharacter(input: CharacterRemoveInput!): Character!
|
||||||
|
|
||||||
|
|
||||||
|
# Add a channel
|
||||||
|
addChannel(input: ChannelAddInput!): Channel!
|
||||||
|
|
||||||
|
# Edit a channel
|
||||||
|
editChannel(input: ChannelEditInput!): Channel!
|
||||||
|
|
||||||
|
|
||||||
|
"Upload a file"
|
||||||
|
uploadFile(input: FileUploadInput): File!
|
||||||
|
|
||||||
|
"Edit a file"
|
||||||
|
editFile(input: FileEditInput): File!
|
||||||
|
|
||||||
|
"Remove a file"
|
||||||
|
removeFile(input: FileRemoveInput): File!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscription {
|
||||||
|
"""
|
||||||
|
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
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
A change represents a a change in the history.
|
||||||
|
"""
|
||||||
|
type Change {
|
||||||
|
"A unique ID of the change."
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
"The model of the changed object."
|
||||||
|
model: ChangeModel!
|
||||||
|
|
||||||
|
"The operation performed."
|
||||||
|
op: String!
|
||||||
|
|
||||||
|
"The author that performed the change."
|
||||||
|
author: String!
|
||||||
|
|
||||||
|
"Whether the change will be listed without a matching key."
|
||||||
|
listed: Boolean!
|
||||||
|
|
||||||
|
"The keys indexing this change."
|
||||||
|
keys: [ChangeKey!]!
|
||||||
|
|
||||||
|
"The date when the change happened."
|
||||||
|
date: Time
|
||||||
|
|
||||||
|
"The changed objects."
|
||||||
|
objects: [ChangeObject!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter | Comment | File
|
||||||
|
|
||||||
|
"""
|
||||||
|
A change key is a key associated with a change, used for filtering
|
||||||
|
and subscription. A log post edit has keys for both the log and post,
|
||||||
|
making it suitable to use it to patch an active log file.
|
||||||
|
"""
|
||||||
|
type ChangeKey {
|
||||||
|
"The model the key is for."
|
||||||
|
model: ChangeModel!
|
||||||
|
|
||||||
|
"The model's id, or * if it's a key indicating a model list should be updated."
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
See ChangeKey
|
||||||
|
"""
|
||||||
|
input ChangeKeyInput {
|
||||||
|
"The model the key is for."
|
||||||
|
model: ChangeModel!
|
||||||
|
|
||||||
|
"The model's id, or * if it's a key indicating a model list should be updated."
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
A model related to the change.
|
||||||
|
"""
|
||||||
|
enum ChangeModel {
|
||||||
|
Character
|
||||||
|
Channel
|
||||||
|
Log
|
||||||
|
Post
|
||||||
|
Story
|
||||||
|
Tag
|
||||||
|
Chapter
|
||||||
|
Comment
|
||||||
|
File
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional filter for the changes query.
|
||||||
|
"""
|
||||||
|
input ChangesFilter {
|
||||||
|
"The keys to query for."
|
||||||
|
keys: [ChangeKeyInput!]
|
||||||
|
|
||||||
|
"Only show changes by this author"
|
||||||
|
author: String
|
||||||
|
|
||||||
|
"Only show changes more recent than this date."
|
||||||
|
earliestDate: Time
|
||||||
|
|
||||||
|
"Limit the amount of results. This even goes for subscriptions!"
|
||||||
|
limit: Int
|
||||||
|
}
|
||||||
@@ -16,6 +16,18 @@ type Channel {
|
|||||||
locationName: String
|
locationName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Filters for the channels query
|
||||||
|
input ChannelsFilter {
|
||||||
|
# Filter to either logged or unlogged channels
|
||||||
|
logged: Boolean
|
||||||
|
|
||||||
|
# Filter by event name
|
||||||
|
eventName: String
|
||||||
|
|
||||||
|
# Filter by location name
|
||||||
|
locationName: String
|
||||||
|
}
|
||||||
|
|
||||||
input ChannelAddInput {
|
input ChannelAddInput {
|
||||||
# The channel's name
|
# The channel's name
|
||||||
name: String!
|
name: String!
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# A Chapter is the main content body of a story.
|
||||||
|
type Chapter {
|
||||||
|
# The chapter's ID
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
# The chapter's title
|
||||||
|
title: String!
|
||||||
|
|
||||||
|
# The chapter's author
|
||||||
|
author: String!
|
||||||
|
|
||||||
|
# The chapter's source
|
||||||
|
source: String!
|
||||||
|
|
||||||
|
# When the chapter was posted initialy
|
||||||
|
createdDate: Time!
|
||||||
|
|
||||||
|
# The in-universe date and time of the chapter
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
# The date of edit.
|
||||||
|
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 ChapterAddInput {
|
||||||
|
# The story to add a chapter to
|
||||||
|
storyId: String!
|
||||||
|
|
||||||
|
# The title to give the chapter. It can not be left empty
|
||||||
|
title: String!
|
||||||
|
|
||||||
|
# The author to set for the chapter. It will use the logged in user ID, and only
|
||||||
|
# users with appropriate permissions can post as another author. Even then, it's only
|
||||||
|
# for importing/moving content. This author name will not be used when logging the
|
||||||
|
# submission
|
||||||
|
author: String
|
||||||
|
|
||||||
|
# The markdown source for the content
|
||||||
|
source: String!
|
||||||
|
|
||||||
|
# Optionally, assign a fictional date for the chapter
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
"The comment mode for the chapter."
|
||||||
|
commentMode: ChapterCommentMode
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for editChapter mutation
|
||||||
|
input ChapterEditInput {
|
||||||
|
# The chapter to edit
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
# Change the chapter's title
|
||||||
|
title: String
|
||||||
|
|
||||||
|
# Change the source
|
||||||
|
source: String
|
||||||
|
|
||||||
|
# Set the fictional date for a chapter
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
# Remove the fictional date for a chapter
|
||||||
|
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 ChapterMoveInput {
|
||||||
|
# The chapter to move
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
# The story to move it to
|
||||||
|
storyId: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for removeChapter mutation
|
||||||
|
input ChapterRemoveInput {
|
||||||
|
# The chapter to remove
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
@@ -22,6 +22,24 @@ type Character {
|
|||||||
description: String!
|
description: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Filter for characters query
|
||||||
|
input CharactersFilter {
|
||||||
|
# Filter by character IDs
|
||||||
|
ids: [String!]
|
||||||
|
|
||||||
|
# Filter by nicks
|
||||||
|
nicks: [String!]
|
||||||
|
|
||||||
|
# Filter by names
|
||||||
|
names: [String!]
|
||||||
|
|
||||||
|
# Filter by author
|
||||||
|
author: String
|
||||||
|
|
||||||
|
# Filter by text search matching against the character's description
|
||||||
|
search: String
|
||||||
|
}
|
||||||
|
|
||||||
# Input for adding characters
|
# Input for adding characters
|
||||||
input CharacterAddInput {
|
input CharacterAddInput {
|
||||||
# The primary IRC nick name to recognize this character by
|
# The primary IRC nick name to recognize this character by
|
||||||
@@ -50,6 +68,7 @@ input CharacterNickInput {
|
|||||||
nick: String!
|
nick: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Inpuit for editCharacter mutation
|
||||||
input CharacterEditInput {
|
input CharacterEditInput {
|
||||||
# The id for the character to edit
|
# The id for the character to edit
|
||||||
id: String!
|
id: String!
|
||||||
@@ -62,4 +81,11 @@ input CharacterEditInput {
|
|||||||
|
|
||||||
# A short description for the character
|
# A short description for the character
|
||||||
description: String
|
description: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Input for removeCharacter mutation
|
||||||
|
input CharacterRemoveInput {
|
||||||
|
# The id for the character to edit
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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: String!
|
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.
|
||||||
@@ -29,9 +29,30 @@ type File {
|
|||||||
url: String
|
url: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Filter for the files quiery.
|
||||||
|
input FilesFilter {
|
||||||
|
# If set, this will limit the results to either public or private files.
|
||||||
|
public: Boolean
|
||||||
|
|
||||||
|
# Limit the MIME types of the files.
|
||||||
|
mimeTypes: [String!]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for 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!
|
||||||
|
|
||||||
# Whether the file is publicly listable. Someone with knowledge of the ID
|
# Whether the file is publicly listable. Someone with knowledge of the ID
|
||||||
@@ -40,4 +61,10 @@ 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: String!
|
date: Time!
|
||||||
|
|
||||||
# The channel of a log.
|
# The channel of a log.
|
||||||
channelName: String!
|
channelName: String!
|
||||||
@@ -21,7 +21,7 @@ type Log {
|
|||||||
|
|
||||||
# The log's event, which is the same as the tags in the previous logbot site.
|
# The log's event, which is the same as the tags in the previous logbot site.
|
||||||
# Empty string means that it's no event.
|
# Empty string means that it's no event.
|
||||||
event: String!
|
eventName: String!
|
||||||
|
|
||||||
# The description of a session, which is empty if unset.
|
# The description of a session, which is empty if unset.
|
||||||
description: String!
|
description: String!
|
||||||
@@ -34,10 +34,24 @@ 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!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for logs query
|
# Suggested log
|
||||||
input LogQueryInput {
|
type LogSuggestion {
|
||||||
|
log: Log!
|
||||||
|
hasEvent: Boolean!
|
||||||
|
hasChannel: Boolean!
|
||||||
|
characters: [Character!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter for logs query
|
||||||
|
input LogsFilter {
|
||||||
# Channels to limit results to (inclusive)
|
# Channels to limit results to (inclusive)
|
||||||
channels: [String!]
|
channels: [String!]
|
||||||
|
|
||||||
@@ -50,17 +64,17 @@ input LogQueryInput {
|
|||||||
# Search post content
|
# Search post content
|
||||||
search: String
|
search: String
|
||||||
|
|
||||||
# Limit to only open logs
|
# Limit by whether it's open or not
|
||||||
open: Boolean
|
open: Boolean
|
||||||
|
|
||||||
# Limit the amount of posts
|
# Limit the amount of results you get back
|
||||||
limit: Int
|
limit: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input for addLog mutation
|
# Input for addLog mutation
|
||||||
input LogAddInput {
|
input LogAddInput {
|
||||||
# The date of the log, in RFC3339 format with up to nanosecond precision
|
# The date of the log.
|
||||||
date: String!
|
date: Time!
|
||||||
|
|
||||||
# The channel where the log is set
|
# The channel where the log is set
|
||||||
channel: String!
|
channel: String!
|
||||||
@@ -96,37 +110,73 @@ input LogEditInput {
|
|||||||
open: Boolean
|
open: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
# A LogHeader is a cut-down version of Log that does not allow getting the actual posts.
|
# Input for removeLog mutation
|
||||||
type LogHeader {
|
input LogRemoveInput {
|
||||||
# A unique identifier for the log.
|
# The id of the log to remove
|
||||||
id: String!
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
# A secondary unique identifier for the log. This is a lot shorter and more suitable for storage when
|
"""
|
||||||
# the link doesn't need to be as expressive.
|
Input for importLog mutation.
|
||||||
shortId: String!
|
"""
|
||||||
|
input LogImportInput {
|
||||||
# The date for a log.
|
"The channel of the log, alwyas required."
|
||||||
date: String!
|
|
||||||
|
|
||||||
# The channel name of a log.
|
|
||||||
channelName: String!
|
channelName: String!
|
||||||
|
|
||||||
# Information about the channel this log is in.
|
"Which importer to use."
|
||||||
channel: Channel!
|
importer: LogImporter!
|
||||||
|
|
||||||
# The session's title.
|
|
||||||
title: String!
|
|
||||||
|
|
||||||
# The log's event, which is the same as the tags in the previous logbot site.
|
"The timezone of the import, all log and post dates will be treated as it. It will be UTC if none is specified."
|
||||||
# Empty string means that it's no event.
|
timezone: String
|
||||||
event: String!
|
|
||||||
|
|
||||||
# The description of a session, which is empty if unset.
|
"The date of the log, if not provided in the log body."
|
||||||
description: String!
|
date: Time
|
||||||
|
|
||||||
# Whether the log session is open.
|
"Session threwhold"
|
||||||
open: Boolean!
|
sessionThresholdMs: Int
|
||||||
|
|
||||||
# The characters involved in the log file.
|
"The log body itself."
|
||||||
characters: [Character!]!
|
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: String!
|
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!
|
||||||
@@ -23,12 +23,12 @@ type Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Input for the addPost mutation
|
# Input for the addPost mutation
|
||||||
input AddPostInput {
|
input PostAddInput {
|
||||||
# The log's ID that this post should be a part of
|
# The log's ID that this post should be a part of
|
||||||
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: String!
|
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!
|
||||||
@@ -41,12 +41,12 @@ input AddPostInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Input for the editPost mutation
|
# Input for the editPost mutation
|
||||||
input EditPostInput {
|
input PostEditInput {
|
||||||
# The Post ID
|
# The Post ID
|
||||||
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: String
|
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
|
||||||
@@ -59,10 +59,24 @@ input EditPostInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Input for the movePost mutation
|
# Input for the movePost mutation
|
||||||
input MovePostInput {
|
input PostMoveInput {
|
||||||
# The Post ID
|
# The Post ID
|
||||||
id: String!
|
id: String!
|
||||||
|
|
||||||
# Target index
|
# Target index
|
||||||
toPosition: Int!
|
toPosition: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for the removePost mutation
|
||||||
|
input PostRemoveInput {
|
||||||
|
# The Post ID
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter for posts query
|
||||||
|
input PostsFilter {
|
||||||
|
ids: [String!]
|
||||||
|
kinds: [String!]
|
||||||
|
logId: String
|
||||||
|
limit: Int
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# A Story is a piece of content that, unlike wiki articles, are not something that should be ordered by time instead of title. It can
|
||||||
|
# contain multiple chapters by one or more autohrs
|
||||||
|
type Story {
|
||||||
|
# The story's ID
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
# The story's name, which will show up in the lists.
|
||||||
|
name: String!
|
||||||
|
|
||||||
|
# The story's author
|
||||||
|
author: String!
|
||||||
|
|
||||||
|
# Whether other users may add/edit chapters to the story.
|
||||||
|
open: Boolean!
|
||||||
|
|
||||||
|
# Whether users without a direct link can find this story.
|
||||||
|
listed: Boolean!
|
||||||
|
|
||||||
|
# The category of the story
|
||||||
|
category: StoryCategory!
|
||||||
|
|
||||||
|
# The tags for this tory
|
||||||
|
tags: [Tag!]!
|
||||||
|
|
||||||
|
# The chapters of this story
|
||||||
|
chapters: [Chapter!]!
|
||||||
|
|
||||||
|
# The date the story was created.
|
||||||
|
createdDate: Time!
|
||||||
|
|
||||||
|
# The date the story is set in.
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
# The date of the last major update to the story. This being bumped means a new chapter has been added.
|
||||||
|
updatedDate: Time!
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter for stories query.
|
||||||
|
input StoriesFilter {
|
||||||
|
# What author to query for
|
||||||
|
author: String
|
||||||
|
|
||||||
|
# What category to query for
|
||||||
|
category: StoryCategory
|
||||||
|
|
||||||
|
# What tags to query for
|
||||||
|
tags: [TagInput!]
|
||||||
|
|
||||||
|
# If true, it will only show unlisted page you have access to
|
||||||
|
unlisted: Boolean
|
||||||
|
|
||||||
|
# Whether the page is open for additions by other users
|
||||||
|
open: Boolean
|
||||||
|
|
||||||
|
# The earliest fictionalDate
|
||||||
|
earliestFictionalDate: Time
|
||||||
|
|
||||||
|
# The latest fictionalDate
|
||||||
|
latestFictionalDate: Time
|
||||||
|
|
||||||
|
# The max amount of stories to get (default: 30)
|
||||||
|
limit: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for the addStory mutation
|
||||||
|
input StoryAddInput {
|
||||||
|
# Set the name of the story, which will show up as the title, and as a suggestion for
|
||||||
|
# the first chapter being added.
|
||||||
|
name: String!
|
||||||
|
|
||||||
|
# Set the category for the new story.
|
||||||
|
category: StoryCategory!
|
||||||
|
|
||||||
|
# Add the story under another name. This requires the story.add permission, and should not be
|
||||||
|
# abused. The action will be logged with the actual creator's user ID.
|
||||||
|
author: String
|
||||||
|
|
||||||
|
# Allow other users to post chapters to the story.
|
||||||
|
open: Boolean
|
||||||
|
|
||||||
|
# Allow other users to see this page in any lists or search results.
|
||||||
|
listed: Boolean
|
||||||
|
|
||||||
|
# Set which tags the story should have.
|
||||||
|
tags: [TagInput!]
|
||||||
|
|
||||||
|
# Set the fictional date of the story.
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for the editStory mutation
|
||||||
|
input StoryEditInput {
|
||||||
|
# What story to edit
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
# Set the name of the story, which will show up as the title, and as a suggestion for
|
||||||
|
# the first chapter being added.
|
||||||
|
name: String
|
||||||
|
|
||||||
|
# Set the category for the new story.
|
||||||
|
category: StoryCategory
|
||||||
|
|
||||||
|
# Add the story under another name. This requires the story.add permission, and should not be
|
||||||
|
# abused. The action will be logged with the actual creator's user ID.
|
||||||
|
author: String
|
||||||
|
|
||||||
|
# Change whether to allow others to add chapters
|
||||||
|
open: Boolean
|
||||||
|
|
||||||
|
# Change whether to show this story in the list and search results
|
||||||
|
listed: Boolean
|
||||||
|
|
||||||
|
# Set the fictional date of the story.
|
||||||
|
fictionalDate: Time
|
||||||
|
|
||||||
|
# Clear the fictional date of the story.
|
||||||
|
clearFictionalDate: Boolean
|
||||||
|
|
||||||
|
# Whether to sort by fictional date.
|
||||||
|
sortByFictionalDate: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for the addStoryTag mutation
|
||||||
|
input StoryTagAddInput {
|
||||||
|
# What story to add the tag to.
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
# The tag to add.
|
||||||
|
tag: TagInput!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for the removeStoryTag mutation
|
||||||
|
input StoryTagRemoveInput {
|
||||||
|
# What story to remove the tag from.
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
# The tag to remove.
|
||||||
|
tag: TagInput!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input for the removeStory mutation
|
||||||
|
input StoryRemoveInput {
|
||||||
|
# What story to remove.
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Possible values for Story.category
|
||||||
|
enum StoryCategory {
|
||||||
|
# General information
|
||||||
|
Info
|
||||||
|
|
||||||
|
# News stories
|
||||||
|
News
|
||||||
|
|
||||||
|
# Description and content of a document or item
|
||||||
|
Document
|
||||||
|
|
||||||
|
# Information about something going on in the background that may or may not inform RP
|
||||||
|
Background
|
||||||
|
|
||||||
|
# A short story
|
||||||
|
Story
|
||||||
|
}
|
||||||
@@ -7,6 +7,24 @@ type Tag {
|
|||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A Tag is a means of associating stories that have details in common with one another.
|
||||||
|
input TagInput {
|
||||||
|
# The tag's kind
|
||||||
|
kind: TagKind!
|
||||||
|
|
||||||
|
# The tag's name
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
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...
|
||||||
@@ -24,4 +42,5 @@ enum TagKind {
|
|||||||
|
|
||||||
# None of the above, but it does still tie multiple stories together. The new story/chapter format may obsolete this tag kind.
|
# None of the above, but it does still tie multiple stories together. The new story/chapter format may obsolete this tag kind.
|
||||||
Series
|
Series
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Token represents the data parsed and validated from the bearer token.
|
||||||
|
type Token {
|
||||||
|
# The user represented in the token.
|
||||||
|
user: User!
|
||||||
|
|
||||||
|
# The permissions for the token.
|
||||||
|
permissions: [String!]!
|
||||||
|
}
|
||||||
@@ -0,0 +1,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!
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# A user represents a member on the RPData platform.
|
||||||
|
type User {
|
||||||
|
# The user's unique ID / username
|
||||||
|
id: String!
|
||||||
|
|
||||||
|
# What the user is permitted to do.
|
||||||
|
permissions: [String!]!
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type changeResolver struct{}
|
||||||
|
|
||||||
|
func (r *changeResolver) Objects(ctx context.Context, obj *models.Change) ([]graphcore.ChangeObject, error) {
|
||||||
|
objects := obj.Objects()
|
||||||
|
|
||||||
|
results := make([]graphcore.ChangeObject, 0, len(objects))
|
||||||
|
for _, object := range objects {
|
||||||
|
if changeObject, ok := object.(graphcore.ChangeObject); ok {
|
||||||
|
results = append(results, changeObject)
|
||||||
|
} else {
|
||||||
|
log.Printf("Type %T needs ChangeObject impl", object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeResolver is a resolver
|
||||||
|
var ChangeResolver changeResolver
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chapterResolver struct {
|
||||||
|
stories *services.StoryService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Chapter) (*time.Time, error) {
|
||||||
|
if chapter.FictionalDate.IsZero() {
|
||||||
|
return nil, 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
|
||||||
|
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}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logResolver struct {
|
||||||
|
logs *services.LogService
|
||||||
|
characters *services.CharacterService
|
||||||
|
channels *services.ChannelService
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if len(log.CharacterIDs) == 0 {
|
||||||
|
return []*models.Character{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
func LogResolver(s *services.Bundle) *logResolver {
|
||||||
|
return &logResolver{characters: s.Characters, logs: s.Logs, channels: s.Channels}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type storyResolver struct {
|
||||||
|
stories *services.StoryService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) {
|
||||||
|
if story.FictionalDate.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &story.FictionalDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]*models.Chapter, error) {
|
||||||
|
return r.stories.ListChapters(ctx, *story)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoryResolver is a resolver
|
||||||
|
func StoryResolver(s *services.Bundle) *storyResolver {
|
||||||
|
return &storyResolver{stories: s.Stories}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/services"
|
||||||
|
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tokenResolver struct {
|
||||||
|
auth *services.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *tokenResolver) User(ctx context.Context, token *models.Token) (*models.User, error) {
|
||||||
|
return r.auth.FindUser(ctx, token.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenResolver is a resolver
|
||||||
|
func TokenResolver(s *services.Bundle) *tokenResolver {
|
||||||
|
return &tokenResolver{auth: s.Auth}
|
||||||
|
}
|
||||||
+54
-25
@@ -5,55 +5,84 @@ 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"`
|
||||||
|
RestoreIDs bool `json:"restoreIDs" yaml:"restoreIDs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.NewDecoder(file).Decode(config)
|
if strings.HasSuffix(filename, ".json") {
|
||||||
|
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 +93,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,20 +0,0 @@
|
|||||||
package session
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type contextKeyType struct{ name string }
|
|
||||||
|
|
||||||
func (ck *contextKeyType) String() string {
|
|
||||||
return ck.name
|
|
||||||
}
|
|
||||||
|
|
||||||
var contextKey = &contextKeyType{name: "session context key"}
|
|
||||||
|
|
||||||
// FromContext gets a session fron the context.
|
|
||||||
func FromContext(ctx context.Context) *Session {
|
|
||||||
return ctx.Value(contextKey).(*Session)
|
|
||||||
}
|
|
||||||
|
|
||||||
func contextWithSession(parent context.Context, session *Session) context.Context {
|
|
||||||
return context.WithValue(parent, contextKey, session)
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package session
|
|
||||||
|
|
||||||
// DefaultPermissions gets the default permissions
|
|
||||||
func DefaultPermissions() []string {
|
|
||||||
return []string{
|
|
||||||
"member",
|
|
||||||
"log.edit",
|
|
||||||
"log.reorder",
|
|
||||||
"post.edit",
|
|
||||||
"post.move",
|
|
||||||
"file.upload",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.edit": "Can edit non-owned stories",
|
|
||||||
"story.remove": "Can remove non-owned stories",
|
|
||||||
"file.upload": "Can upload files",
|
|
||||||
"file.edit": "Can edit non-owned files",
|
|
||||||
"file.remove": "Can remove non-owned files",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
package session
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/aiterp/wikiauth"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/config"
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var sessionCollection *mgo.Collection
|
|
||||||
|
|
||||||
// A Session represents a login session.
|
|
||||||
type Session struct {
|
|
||||||
mutex sync.Mutex
|
|
||||||
|
|
||||||
ID string `bson:"_id"`
|
|
||||||
Time time.Time `bson:"time"`
|
|
||||||
UserID string `bson:"userId"`
|
|
||||||
|
|
||||||
user *User
|
|
||||||
w http.ResponseWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load loads a session from a cookie, returning either `r` or a request
|
|
||||||
// with the session context.
|
|
||||||
func Load(w http.ResponseWriter, r *http.Request) *http.Request {
|
|
||||||
cookie, err := r.Cookie("aiterp_session")
|
|
||||||
if err != nil {
|
|
||||||
return r.WithContext(contextWithSession(r.Context(), &Session{w: w}))
|
|
||||||
}
|
|
||||||
|
|
||||||
id := cookie.Value
|
|
||||||
|
|
||||||
session := Session{}
|
|
||||||
err = sessionCollection.FindId(id).One(&session)
|
|
||||||
if err != nil || time.Since(session.Time) > time.Hour*168 {
|
|
||||||
return r.WithContext(contextWithSession(r.Context(), &Session{w: w}))
|
|
||||||
}
|
|
||||||
|
|
||||||
if session.ID != "" && time.Since(session.Time) > time.Second*30 {
|
|
||||||
session.Time = time.Now()
|
|
||||||
go sessionCollection.UpdateId(id, bson.M{"$set": bson.M{"time": session.Time}})
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie.Expires = time.Now().Add(time.Hour * 168)
|
|
||||||
http.SetCookie(w, cookie)
|
|
||||||
|
|
||||||
session.w = w
|
|
||||||
|
|
||||||
return r.WithContext(contextWithSession(r.Context(), &session))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login logs a user in.
|
|
||||||
func (session *Session) Login(username, password string) error {
|
|
||||||
auth := wikiauth.New(config.Global().Wiki.URL)
|
|
||||||
|
|
||||||
err := auth.Login(username, password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow bot passwords
|
|
||||||
username = strings.SplitN(username, "@", 2)[0]
|
|
||||||
|
|
||||||
data := make([]byte, 32)
|
|
||||||
_, err = rand.Read(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
session.ID = hex.EncodeToString(data)
|
|
||||||
session.UserID = username
|
|
||||||
session.Time = time.Now()
|
|
||||||
|
|
||||||
err = sessionCollection.Insert(&session)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(session.w, &http.Cookie{
|
|
||||||
Name: "aiterp_session",
|
|
||||||
Value: session.ID,
|
|
||||||
Expires: time.Now().Add(time.Hour * 2160), // 90 days
|
|
||||||
HttpOnly: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
user, err := FindUser(session.UserID)
|
|
||||||
if err == mgo.ErrNotFound {
|
|
||||||
user = User{ID: username, Nick: "", Permissions: DefaultPermissions()}
|
|
||||||
|
|
||||||
err := userCollection.Insert(user)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout logs out the session
|
|
||||||
func (session *Session) Logout() {
|
|
||||||
http.SetCookie(session.w, &http.Cookie{
|
|
||||||
Name: "aiterp_session",
|
|
||||||
Value: "",
|
|
||||||
Expires: time.Unix(0, 0),
|
|
||||||
HttpOnly: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
session.mutex.Lock()
|
|
||||||
session.user = nil
|
|
||||||
session.UserID = ""
|
|
||||||
session.ID = ""
|
|
||||||
session.mutex.Unlock()
|
|
||||||
|
|
||||||
sessionCollection.RemoveId(session.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// User gets the user information for the session.
|
|
||||||
func (session *Session) User() *User {
|
|
||||||
session.mutex.Lock()
|
|
||||||
defer session.mutex.Unlock()
|
|
||||||
|
|
||||||
if session.user != nil {
|
|
||||||
return session.user
|
|
||||||
}
|
|
||||||
|
|
||||||
if session.UserID == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := FindUser(session.UserID)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &user
|
|
||||||
}
|
|
||||||
|
|
||||||
// NameOrPermitted is a shorthand for checking the username OR permissions, e.g. to check
|
|
||||||
// if a logged in user can edit a certain post.
|
|
||||||
func (session *Session) NameOrPermitted(userid string, permissions ...string) bool {
|
|
||||||
if session.UserID == userid {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
user := session.User()
|
|
||||||
if user == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.Permitted()
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
sessionCollection = db.C("core.sessions")
|
|
||||||
|
|
||||||
sessionCollection.EnsureIndexKey("nick")
|
|
||||||
sessionCollection.EnsureIndexKey("userId")
|
|
||||||
|
|
||||||
err := sessionCollection.EnsureIndex(mgo.Index{
|
|
||||||
Name: "time",
|
|
||||||
Key: []string{"time"},
|
|
||||||
ExpireAfter: time.Hour * 168,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var globalCtx, globalCancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// A Task is a wrapper around a function that offers scheduling
|
||||||
|
// and syncronization.
|
||||||
|
type Task struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel context.CancelFunc
|
||||||
|
waitDuration time.Duration
|
||||||
|
scheduled bool
|
||||||
|
callback func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context gets the task's context.
|
||||||
|
func (task *Task) Context() context.Context {
|
||||||
|
return task.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops a task. This should not be done to stop a single run,
|
||||||
|
// but as a way to cancel it forever.
|
||||||
|
func (task *Task) Stop() {
|
||||||
|
task.ctxCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule schedules a task for later execution.
|
||||||
|
func (task *Task) Schedule() {
|
||||||
|
// Don't if the context is closed.
|
||||||
|
select {
|
||||||
|
case <-task.Context().Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
task.mutex.Lock()
|
||||||
|
if task.scheduled {
|
||||||
|
task.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.scheduled = true
|
||||||
|
task.mutex.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-time.After(task.waitDuration)
|
||||||
|
|
||||||
|
// Mark as no longer scheduled
|
||||||
|
task.mutex.Lock()
|
||||||
|
task.scheduled = false
|
||||||
|
task.mutex.Unlock()
|
||||||
|
|
||||||
|
// Don't if the task is cancelled
|
||||||
|
select {
|
||||||
|
case <-task.Context().Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
task.callback()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// New makes a new task with the callback.
|
||||||
|
func New(waitDuration time.Duration, callback func() error) *Task {
|
||||||
|
ctx, ctxCancel := context.WithCancel(globalCtx)
|
||||||
|
|
||||||
|
return &Task{
|
||||||
|
callback: callback,
|
||||||
|
ctx: ctx,
|
||||||
|
ctxCancel: ctxCancel,
|
||||||
|
waitDuration: waitDuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopAll stops all tasks.
|
||||||
|
func StopAll() {
|
||||||
|
globalCancel()
|
||||||
|
}
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/model/channel"
|
|
||||||
"github.com/graph-gophers/dataloader"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Channel gets a character by key
|
|
||||||
func (loader *Loader) Channel(key, value string) (channel.Channel, error) {
|
|
||||||
if !strings.HasPrefix(key, "Channel.") {
|
|
||||||
key = "Channel." + key
|
|
||||||
}
|
|
||||||
|
|
||||||
if loader.loaders[key] == nil {
|
|
||||||
return channel.Channel{}, errors.New("unsupported key")
|
|
||||||
}
|
|
||||||
|
|
||||||
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
|
|
||||||
res, err := thunk()
|
|
||||||
if err != nil {
|
|
||||||
return channel.Channel{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, ok := res.(channel.Channel)
|
|
||||||
if !ok {
|
|
||||||
return channel, errors.New("incorrect type")
|
|
||||||
}
|
|
||||||
|
|
||||||
return channel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func channelNameBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
|
||||||
var results []*dataloader.Result
|
|
||||||
names := keys.Keys()
|
|
||||||
|
|
||||||
channels, err := channel.ListNames(names...)
|
|
||||||
if err != nil {
|
|
||||||
for range names {
|
|
||||||
results = append(results, &dataloader.Result{Data: channel.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: channel.Channel{}, Error: err})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/model/character"
|
|
||||||
"github.com/graph-gophers/dataloader"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Character gets a character by key
|
|
||||||
func (loader *Loader) Character(key, value string) (character.Character, error) {
|
|
||||||
if !strings.HasPrefix(key, "Character.") {
|
|
||||||
key = "Character." + key
|
|
||||||
}
|
|
||||||
|
|
||||||
if loader.loaders[key] == nil {
|
|
||||||
return character.Character{}, errors.New("unsupported key")
|
|
||||||
}
|
|
||||||
|
|
||||||
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
|
|
||||||
res, err := thunk()
|
|
||||||
if err != nil {
|
|
||||||
return character.Character{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
char, ok := res.(character.Character)
|
|
||||||
if !ok {
|
|
||||||
return character.Character{}, errors.New("incorrect type")
|
|
||||||
}
|
|
||||||
|
|
||||||
return char, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Characters gets characters by key
|
|
||||||
func (loader *Loader) Characters(key string, values ...string) ([]character.Character, error) {
|
|
||||||
if !strings.HasPrefix(key, "Character.") {
|
|
||||||
key = "Character." + key
|
|
||||||
}
|
|
||||||
|
|
||||||
if loader.loaders[key] == nil {
|
|
||||||
return nil, errors.New("unsupported 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([]character.Character, len(res))
|
|
||||||
|
|
||||||
for i := range res {
|
|
||||||
char, ok := res[i].(character.Character)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("incorrect type")
|
|
||||||
}
|
|
||||||
|
|
||||||
chars[i] = char
|
|
||||||
}
|
|
||||||
|
|
||||||
return chars, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func characterIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
|
||||||
results := make([]*dataloader.Result, 0, len(keys))
|
|
||||||
ids := keys.Keys()
|
|
||||||
|
|
||||||
characters, err := character.ListIDs(ids...)
|
|
||||||
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: character.Character{}, Error: ErrNotFound})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
|
||||||
var results []*dataloader.Result
|
|
||||||
nicks := keys.Keys()
|
|
||||||
|
|
||||||
characters, err := character.ListNicks(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: character.Character{}, Error: err})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"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 {
|
|
||||||
ctx context.Context
|
|
||||||
loaders map[string]*dataloader.Loader
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)),
|
|
||||||
"Character.nick": dataloader.NewBatchedLoader(characterNickBatch, dataloader.WithWait(time.Millisecond)),
|
|
||||||
"Channel.name": dataloader.NewBatchedLoader(channelNameBatch, dataloader.WithWait(time.Millisecond)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package change
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/model/counter"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var collection *mgo.Collection
|
|
||||||
|
|
||||||
// A Change represents a change in any other model
|
|
||||||
type Change struct {
|
|
||||||
ID int `bson:"_id" json:"id"`
|
|
||||||
Time time.Time `bson:"time" json:"time"`
|
|
||||||
Model string `bson:"model" json:"model"`
|
|
||||||
Op string `bson:"op" json:"op"`
|
|
||||||
Author string `bson:"author,omitempty" json:"author,omitempty"`
|
|
||||||
ObjectID string `bson:"objectId,omitempty" json:"objectId,omitempty"`
|
|
||||||
Data interface{} `bson:"data,omitempty" json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublicModels lists which models can be listed in bulk by anyone.
|
|
||||||
var PublicModels = []string{
|
|
||||||
"Character",
|
|
||||||
"Log",
|
|
||||||
"Post",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit submits a change to the history.
|
|
||||||
func Submit(model, op, author, objectID string, data interface{}) (Change, error) {
|
|
||||||
index, err := counter.Next("auto_increment", "Change")
|
|
||||||
if err != nil {
|
|
||||||
return Change{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
change := Change{
|
|
||||||
ID: index,
|
|
||||||
Time: time.Now(),
|
|
||||||
Model: model,
|
|
||||||
Op: op,
|
|
||||||
Author: author,
|
|
||||||
ObjectID: objectID,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = collection.Insert(&change)
|
|
||||||
if err != nil {
|
|
||||||
return Change{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return change, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
collection = db.C("common.history")
|
|
||||||
|
|
||||||
collection.EnsureIndexKey("model")
|
|
||||||
collection.EnsureIndexKey("author")
|
|
||||||
collection.EnsureIndexKey("objectId")
|
|
||||||
|
|
||||||
err := collection.EnsureIndex(mgo.Index{
|
|
||||||
Name: "expiry",
|
|
||||||
Key: []string{"time"},
|
|
||||||
ExpireAfter: time.Hour * 336,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
package channel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var collection *mgo.Collection
|
|
||||||
|
|
||||||
// ErrInvalidName is an error for an invalid channel name
|
|
||||||
var ErrInvalidName = errors.New("Invalid channel name")
|
|
||||||
|
|
||||||
// A Channel represents information abount an IRC RP channel, and whether it should be logged
|
|
||||||
type Channel struct {
|
|
||||||
Name string `bson:"_id"`
|
|
||||||
Logged bool `bson:"logged"`
|
|
||||||
Hub bool `bson:"hub"`
|
|
||||||
Event string `bson:"event,omitempty"`
|
|
||||||
Location string `bson:"location,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit edits the channel
|
|
||||||
func (channel *Channel) Edit(logged, hub *bool, event, location *string) error {
|
|
||||||
changes := bson.M{}
|
|
||||||
changed := *channel
|
|
||||||
|
|
||||||
if logged != nil && channel.Logged != *logged {
|
|
||||||
changes["logged"] = *logged
|
|
||||||
changed.Logged = *logged
|
|
||||||
}
|
|
||||||
if hub != nil && channel.Hub != *hub {
|
|
||||||
changes["hub"] = *hub
|
|
||||||
changed.Hub = *hub
|
|
||||||
}
|
|
||||||
if event != nil && channel.Event != *event {
|
|
||||||
changes["event"] = *event
|
|
||||||
changed.Event = *event
|
|
||||||
}
|
|
||||||
if location != nil && channel.Event != *location {
|
|
||||||
changes["location"] = *location
|
|
||||||
changed.Location = *location
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(changes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := collection.UpdateId(channel.Name, bson.M{"$set": changes})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*channel = changed
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the channel information from the database.
|
|
||||||
func (channel *Channel) Remove() error {
|
|
||||||
return collection.RemoveId(channel.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure ensures a channel's existence. It does not change `logged` if there is
|
|
||||||
// an existing channel.
|
|
||||||
func Ensure(name string, logged bool) (Channel, error) {
|
|
||||||
channel, err := FindName(name)
|
|
||||||
if err == mgo.ErrNotFound {
|
|
||||||
return New(name, logged, false, "", "")
|
|
||||||
} else if err != nil {
|
|
||||||
return Channel{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return channel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new channel
|
|
||||||
func New(name string, logged, hub bool, event, location string) (Channel, error) {
|
|
||||||
if len(name) < 3 && !strings.HasPrefix(name, "#") {
|
|
||||||
return Channel{}, ErrInvalidName
|
|
||||||
}
|
|
||||||
|
|
||||||
channel := Channel{
|
|
||||||
Name: name,
|
|
||||||
Logged: logged,
|
|
||||||
Hub: hub,
|
|
||||||
Event: event,
|
|
||||||
Location: location,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := collection.Insert(channel)
|
|
||||||
if err != nil {
|
|
||||||
return Channel{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return channel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindName finds a channel by its id (its name).
|
|
||||||
func FindName(name string) (Channel, error) {
|
|
||||||
channel := Channel{}
|
|
||||||
err := collection.FindId(name).One(&channel)
|
|
||||||
|
|
||||||
return channel, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// List finds channels, if logged is true it will be limited to logged
|
|
||||||
// channels
|
|
||||||
func List(logged bool) ([]Channel, error) {
|
|
||||||
query := bson.M{}
|
|
||||||
if logged {
|
|
||||||
query["logged"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
channels := make([]Channel, 0, 32)
|
|
||||||
err := collection.Find(query).All(&channels)
|
|
||||||
|
|
||||||
return channels, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListNames finds channels by the names provided
|
|
||||||
func ListNames(names ...string) ([]Channel, error) {
|
|
||||||
query := bson.M{"_id": bson.M{"$in": names}}
|
|
||||||
|
|
||||||
channels := make([]Channel, 0, 32)
|
|
||||||
err := collection.Find(query).All(&channels)
|
|
||||||
|
|
||||||
return channels, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
collection = db.C("common.channels")
|
|
||||||
|
|
||||||
collection.EnsureIndexKey("logged")
|
|
||||||
collection.EnsureIndexKey("hub")
|
|
||||||
collection.EnsureIndexKey("event")
|
|
||||||
collection.EnsureIndexKey("location")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
package character
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/model/counter"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var collection *mgo.Collection
|
|
||||||
|
|
||||||
// Character is a common data model representing an RP character or NPC.
|
|
||||||
type Character struct {
|
|
||||||
ID string `json:"id" bson:"_id"`
|
|
||||||
Nicks []string `json:"nicks" bson:"nicks"`
|
|
||||||
Name string `json:"name" bson:"name"`
|
|
||||||
ShortName string `json:"shortName" bson:"shortName"`
|
|
||||||
Author string `json:"author" bson:"author"`
|
|
||||||
Description string `json:"description" bson:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasNick returns true if the character has that nick
|
|
||||||
func (character *Character) HasNick(nick string) bool {
|
|
||||||
for i := range character.Nicks {
|
|
||||||
if strings.EqualFold(character.Nicks[i], nick) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddNick adds a nick to the character. It will return an error
|
|
||||||
// if the nick already exists.
|
|
||||||
func (character *Character) AddNick(nick string) error {
|
|
||||||
for i := range character.Nicks {
|
|
||||||
if strings.EqualFold(character.Nicks[i], nick) {
|
|
||||||
return errors.New("Nick already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := collection.UpdateId(character.ID, bson.M{"$push": bson.M{"nicks": nick}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
character.Nicks = append(character.Nicks, nick)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveNick removes the nick from the character. It will raise
|
|
||||||
// an error if the nick does not exist; even if that kind of is
|
|
||||||
// the end goal.
|
|
||||||
func (character *Character) RemoveNick(nick string) error {
|
|
||||||
index := -1
|
|
||||||
for i := range character.Nicks {
|
|
||||||
if strings.EqualFold(character.Nicks[i], nick) {
|
|
||||||
index = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if index == -1 {
|
|
||||||
return errors.New("Nick does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := collection.UpdateId(character.ID, bson.M{"$pull": bson.M{"nicks": nick}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
character.Nicks = append(character.Nicks[:index], character.Nicks[index+1:]...)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit sets the fields of metadata. Only non-empty and different fields will be set in the
|
|
||||||
// database, preventing out of order edits to two fields from conflicting
|
|
||||||
func (character *Character) Edit(name, shortName, description string) error {
|
|
||||||
changes := bson.M{}
|
|
||||||
if len(name) > 0 && name != character.Name {
|
|
||||||
changes["name"] = name
|
|
||||||
}
|
|
||||||
if len(shortName) > 0 && shortName != character.ShortName {
|
|
||||||
changes["shortName"] = shortName
|
|
||||||
}
|
|
||||||
if len(description) > 0 && description != character.Description {
|
|
||||||
changes["description"] = description
|
|
||||||
}
|
|
||||||
|
|
||||||
err := collection.UpdateId(character.ID, changes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if changes["name"] != nil {
|
|
||||||
character.Name = name
|
|
||||||
}
|
|
||||||
if changes["shortName"] != nil {
|
|
||||||
character.ShortName = shortName
|
|
||||||
}
|
|
||||||
if changes["description"] != nil {
|
|
||||||
character.Description = description
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the character from the database. The reason this is an instance method
|
|
||||||
// is that it should only be done after an authorization check.
|
|
||||||
func (character *Character) Remove() error {
|
|
||||||
return collection.RemoveId(character.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindID finds Character by ID
|
|
||||||
func FindID(id string) (Character, error) {
|
|
||||||
return find(bson.M{"_id": id})
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindNick finds Character by nick
|
|
||||||
func FindNick(nick string) (Character, error) {
|
|
||||||
return find(bson.M{"nicks": nick})
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindName finds Character by either full name or
|
|
||||||
// short name.
|
|
||||||
func FindName(name string) (Character, error) {
|
|
||||||
return find(bson.M{"$or": []bson.M{bson.M{"name": name}, bson.M{"shortName": name}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// List lists all characters
|
|
||||||
func List() ([]Character, error) {
|
|
||||||
return list(bson.M{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAuthor lists all characters by author
|
|
||||||
func ListAuthor(author string) ([]Character, error) {
|
|
||||||
return list(bson.M{"author": author})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListNicks lists all characters with either of these nicks. This was made with
|
|
||||||
// the logbot in mind, to batch an order for characters.
|
|
||||||
func ListNicks(nicks ...string) ([]Character, error) {
|
|
||||||
return list(bson.M{"nicks": bson.M{"$in": nicks}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListIDs lists all characters with either of these IDs.
|
|
||||||
func ListIDs(ids ...string) ([]Character, error) {
|
|
||||||
return list(bson.M{"_id": bson.M{"$in": ids}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a Character and pushes it to the database. It does some validation
|
|
||||||
// on nick, name, shortName and author. Leave the shortname blank to have it be the
|
|
||||||
// first name.
|
|
||||||
func New(nick, name, shortName, author, description string) (Character, error) {
|
|
||||||
if len(nick) < 1 || len(name) < 1 || len(author) < 1 {
|
|
||||||
return Character{}, errors.New("Nick, name, or author name too short or empty")
|
|
||||||
}
|
|
||||||
if shortName == "" {
|
|
||||||
shortName = strings.SplitN(name, " ", 2)[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
char, err := FindNick(nick)
|
|
||||||
if err == nil && char.ID != "" {
|
|
||||||
return Character{}, errors.New("Nick is occupied")
|
|
||||||
}
|
|
||||||
|
|
||||||
nextID, err := counter.Next("auto_increment", "Character")
|
|
||||||
if err != nil {
|
|
||||||
return Character{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
character := Character{
|
|
||||||
ID: "C" + strconv.Itoa(nextID),
|
|
||||||
Nicks: []string{nick},
|
|
||||||
Name: name,
|
|
||||||
ShortName: shortName,
|
|
||||||
Author: author,
|
|
||||||
Description: description,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = collection.Insert(character)
|
|
||||||
if err != nil {
|
|
||||||
return Character{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return character, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func find(query interface{}) (Character, error) {
|
|
||||||
character := Character{}
|
|
||||||
err := collection.Find(query).One(&character)
|
|
||||||
if err != nil {
|
|
||||||
return Character{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return character, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func list(query interface{}) ([]Character, error) {
|
|
||||||
characters := make([]Character, 0, 64)
|
|
||||||
err := collection.Find(query).All(&characters)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return characters, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
collection = db.C("common.characters")
|
|
||||||
|
|
||||||
collection.EnsureIndexKey("name")
|
|
||||||
collection.EnsureIndexKey("shortName")
|
|
||||||
collection.EnsureIndexKey("author")
|
|
||||||
err := collection.EnsureIndex(mgo.Index{
|
|
||||||
Key: []string{"nicks"},
|
|
||||||
Unique: true,
|
|
||||||
DropDups: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("init common.characters:", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
package file
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var fileCollection *mgo.Collection
|
|
||||||
|
|
||||||
// A File is a record of a file stored in the Space.
|
|
||||||
type File struct {
|
|
||||||
ID string `bson:"_id" json:"id"`
|
|
||||||
Time time.Time `bson:"time" json:"time"`
|
|
||||||
Kind string `bson:"kind" json:"kind"`
|
|
||||||
Public bool `bson:"public" json:"public"`
|
|
||||||
Name string `bson:"name" json:"name"`
|
|
||||||
MimeType string `bson:"mimeType" json:"mimeType"`
|
|
||||||
Size int64 `bson:"size" json:"size"`
|
|
||||||
Author string `bson:"author" json:"author"`
|
|
||||||
URL string `bson:"url,omitempty" json:"url,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit edits the file, changing up to both the two mutable properties
|
|
||||||
func (file *File) Edit(name *string, public *bool) error {
|
|
||||||
changes := bson.M{}
|
|
||||||
changedFile := *file
|
|
||||||
|
|
||||||
if name != nil && *name != file.Name {
|
|
||||||
changes["name"] = *name
|
|
||||||
changedFile.Name = *name
|
|
||||||
}
|
|
||||||
if public != nil && *public != file.Public {
|
|
||||||
changes["public"] = *public
|
|
||||||
changedFile.Public = *public
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(changes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := fileCollection.UpdateId(file.ID, bson.M{"$set": changes})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*file = changedFile
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the file information from the database, and deletes the file.
|
|
||||||
func (file *File) Delete() error {
|
|
||||||
err := fileCollection.RemoveId(file.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.Kind == "upload" {
|
|
||||||
err = store.RemoveFile("files", file.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file.URL = ""
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert manually inserts file information into the database. This should never, ever be API accessible
|
|
||||||
func Insert(name, kind, mimeType, author string, time time.Time, size int64, url string) (File, error) {
|
|
||||||
file := File{
|
|
||||||
ID: makeFileID(),
|
|
||||||
Kind: kind,
|
|
||||||
Time: time,
|
|
||||||
Public: false,
|
|
||||||
Author: author,
|
|
||||||
Name: name,
|
|
||||||
MimeType: mimeType,
|
|
||||||
Size: size,
|
|
||||||
URL: url,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := fileCollection.Insert(file)
|
|
||||||
if err != nil {
|
|
||||||
return File{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload adds a file to the space.
|
|
||||||
func Upload(ctx context.Context, name, mimeType, author string, size int64, input io.Reader) (File, error) {
|
|
||||||
if !allowdMimeTypes[mimeType] {
|
|
||||||
return File{}, errors.New("File type not allowed:" + mimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if name == "" {
|
|
||||||
date := time.Now().UTC().Format("Jan 02 2006 15:04:05 MST")
|
|
||||||
name = "Unnamed file (" + date + ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
if mimeType == "" {
|
|
||||||
mimeType = "binary/octet-stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
id := makeFileID()
|
|
||||||
|
|
||||||
path, err := store.UploadFile(ctx, "files", id, mimeType, input, size)
|
|
||||||
if err != nil {
|
|
||||||
return File{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
file := File{
|
|
||||||
ID: id,
|
|
||||||
Kind: "upload",
|
|
||||||
Time: time.Now(),
|
|
||||||
Public: false,
|
|
||||||
Author: author,
|
|
||||||
Name: name,
|
|
||||||
MimeType: mimeType,
|
|
||||||
Size: size,
|
|
||||||
URL: store.URLFromPath(path),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = fileCollection.Insert(file)
|
|
||||||
if err != nil {
|
|
||||||
return File{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindID finds a file by ID
|
|
||||||
func FindID(id string) (File, error) {
|
|
||||||
file := File{}
|
|
||||||
|
|
||||||
err := fileCollection.FindId(id).One(&file)
|
|
||||||
if err != nil {
|
|
||||||
return File{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindName finds a file by kind, name and author
|
|
||||||
func FindName(kind string, name string, author string) (File, error) {
|
|
||||||
query := bson.M{"kind": kind, "name": name, "author": author}
|
|
||||||
file := File{}
|
|
||||||
|
|
||||||
err := fileCollection.Find(query).One(&file)
|
|
||||||
if err != nil {
|
|
||||||
return File{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List lists files according to the standard lookup. By default it's just the author's own files,
|
|
||||||
// but if `public` is true it will alos include files made public by other authors. If `mimeTypes` contains
|
|
||||||
// any, it will limit the results to that. If `author` is empty, it will only list public files
|
|
||||||
func List(author string, public bool, mimeTypes []string) ([]File, error) {
|
|
||||||
query := bson.M{}
|
|
||||||
|
|
||||||
if author != "" {
|
|
||||||
if public {
|
|
||||||
query["$or"] = []bson.M{
|
|
||||||
bson.M{"author": author},
|
|
||||||
bson.M{"public": true},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query["author"] = author
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !public {
|
|
||||||
return nil, errors.New("No author specified, and public is unset")
|
|
||||||
}
|
|
||||||
|
|
||||||
query["public"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mimeTypes) > 0 {
|
|
||||||
query["mimeTypes"] = bson.M{"$in": mimeTypes}
|
|
||||||
}
|
|
||||||
|
|
||||||
return listFiles(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
func listFiles(query interface{}) ([]File, error) {
|
|
||||||
list := make([]File, 0, 32)
|
|
||||||
|
|
||||||
err := fileCollection.Find(query).Sort("-time").All(&list)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeFileID makes a random file ID that's 32 characters long
|
|
||||||
func makeFileID() string {
|
|
||||||
result := "F" + strconv.FormatInt(time.Now().UnixNano(), 36)
|
|
||||||
offset := 0
|
|
||||||
data := make([]byte, 32)
|
|
||||||
|
|
||||||
rand.Read(data)
|
|
||||||
for len(result) < 32 {
|
|
||||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
|
||||||
offset += 8
|
|
||||||
|
|
||||||
if offset >= 32 {
|
|
||||||
rand.Read(data)
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result[:32]
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
fileCollection = db.C("file.headers")
|
|
||||||
|
|
||||||
fileCollection.EnsureIndexKey("author")
|
|
||||||
fileCollection.EnsureIndexKey("public")
|
|
||||||
fileCollection.EnsureIndexKey("kind", "name", "author")
|
|
||||||
fileCollection.EnsureIndexKey("author", "public")
|
|
||||||
fileCollection.EnsureIndexKey("kind")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowdMimeTypes = map[string]bool{
|
|
||||||
"": false,
|
|
||||||
"image/jpeg": true,
|
|
||||||
"image/png": true,
|
|
||||||
"image/gif": true,
|
|
||||||
"text/plain": true,
|
|
||||||
"application/json": true,
|
|
||||||
"application/pdf": false,
|
|
||||||
"binary/octet-stream": false,
|
|
||||||
"video/mp4": false,
|
|
||||||
"audio/mp3": false,
|
|
||||||
}
|
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
package log
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"git.aiterp.net/rpdata/api/model/channel"
|
|
||||||
"git.aiterp.net/rpdata/api/model/character"
|
|
||||||
"git.aiterp.net/rpdata/api/model/counter"
|
|
||||||
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var postMutex sync.RWMutex
|
|
||||||
|
|
||||||
var characterUpdateMutex sync.Mutex
|
|
||||||
|
|
||||||
var logsCollection *mgo.Collection
|
|
||||||
|
|
||||||
// Log is the header/session for a log file.
|
|
||||||
type Log struct {
|
|
||||||
ID string `bson:"_id"`
|
|
||||||
ShortID string `bson:"shortId"`
|
|
||||||
Date time.Time `bson:"date"`
|
|
||||||
Channel string `bson:"channel"`
|
|
||||||
Title string `bson:"title,omitempty"`
|
|
||||||
Event string `bson:"event,omitempty"`
|
|
||||||
Description string `bson:"description,omitempty"`
|
|
||||||
Open bool `bson:"open"`
|
|
||||||
CharacterIDs []string `bson:"characterIds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Log
|
|
||||||
func New(date time.Time, channelName, title, event, description string, open bool) (Log, error) {
|
|
||||||
nextID, err := counter.Next("auto_increment", "Log")
|
|
||||||
if err != nil {
|
|
||||||
return Log{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = channel.Ensure(channelName, open)
|
|
||||||
if err != nil {
|
|
||||||
return Log{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log := Log{
|
|
||||||
ID: MakeLogID(date, channelName),
|
|
||||||
ShortID: "L" + strconv.Itoa(nextID),
|
|
||||||
Date: date,
|
|
||||||
Channel: channelName,
|
|
||||||
Title: title,
|
|
||||||
Event: event,
|
|
||||||
Description: description,
|
|
||||||
Open: open,
|
|
||||||
CharacterIDs: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = logsCollection.Insert(log)
|
|
||||||
if err != nil {
|
|
||||||
return Log{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return log, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindID finds a log either by it's ID or short ID.
|
|
||||||
func FindID(id string) (Log, error) {
|
|
||||||
return findLog(bson.M{
|
|
||||||
"$or": []bson.M{
|
|
||||||
bson.M{"_id": id},
|
|
||||||
bson.M{"shortId": id},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// List lists all logs
|
|
||||||
func List(limit int) ([]Log, error) {
|
|
||||||
return listLog(bson.M{}, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the log post with this ID. Both the long and short ID is accepted
|
|
||||||
func Remove(id string) error {
|
|
||||||
return logsCollection.Remove(bson.M{
|
|
||||||
"$or": []bson.M{
|
|
||||||
bson.M{"_id": id},
|
|
||||||
bson.M{"shortId": id},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListSearch lists the logs matching the parameters. Empty/zero values means the parameter is ingored when
|
|
||||||
// building the query. This is the old aitelogs2 way, but with the addition of a text search.
|
|
||||||
//
|
|
||||||
// If a text search is specified, it will make two trips to the database.
|
|
||||||
func ListSearch(textSearch string, channels []string, characterIds []string, events []string, open bool, limit int) ([]Log, error) {
|
|
||||||
postMutex.RLock()
|
|
||||||
defer postMutex.RUnlock()
|
|
||||||
|
|
||||||
query := bson.M{}
|
|
||||||
|
|
||||||
// Run a text search
|
|
||||||
if textSearch != "" {
|
|
||||||
searchResults := make([]string, 0, 32)
|
|
||||||
|
|
||||||
err := postCollection.Find(bson.M{"$text": bson.M{"$search": textSearch}}).Distinct("logId", &searchResults)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Posts always use shortId to refer to the log
|
|
||||||
query["shortId"] = bson.M{"$in": searchResults}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find logs including any of the specified events and channels
|
|
||||||
if len(channels) > 0 {
|
|
||||||
query["channel"] = bson.M{"$in": channels}
|
|
||||||
}
|
|
||||||
if len(events) > 0 {
|
|
||||||
query["events"] = bson.M{"$in": channels}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find logs including all of the specified character IDs.
|
|
||||||
if len(characterIds) > 0 {
|
|
||||||
query["characterIds"] = bson.M{"$all": characterIds}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit to only open logs
|
|
||||||
if open {
|
|
||||||
query["open"] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return listLog(query, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit sets the metadata
|
|
||||||
func (log *Log) Edit(title *string, event *string, description *string, open *bool) error {
|
|
||||||
changes := bson.M{}
|
|
||||||
|
|
||||||
if title != nil && *title != log.Title {
|
|
||||||
changes["title"] = *title
|
|
||||||
}
|
|
||||||
if event != nil && *event != log.Event {
|
|
||||||
changes["event"] = *event
|
|
||||||
}
|
|
||||||
if description != nil && *description != log.Description {
|
|
||||||
changes["description"] = *description
|
|
||||||
}
|
|
||||||
if open != nil && *open != log.Open {
|
|
||||||
changes["open"] = *open
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(changes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := logsCollection.UpdateId(log.ID, bson.M{"$set": changes})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if title != nil {
|
|
||||||
log.Title = *title
|
|
||||||
}
|
|
||||||
if event != nil {
|
|
||||||
log.Event = *event
|
|
||||||
}
|
|
||||||
if description != nil {
|
|
||||||
log.Description = *description
|
|
||||||
}
|
|
||||||
if open != nil {
|
|
||||||
log.Open = *open
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Posts gets all the posts under the log. If no kinds are specified, it
|
|
||||||
// will get all posts
|
|
||||||
func (log *Log) Posts(kinds ...string) ([]Post, error) {
|
|
||||||
postMutex.RLock()
|
|
||||||
defer postMutex.RUnlock()
|
|
||||||
|
|
||||||
query := bson.M{
|
|
||||||
"$or": []bson.M{
|
|
||||||
bson.M{"logId": log.ID},
|
|
||||||
bson.M{"logId": log.ShortID},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(kinds) > 0 {
|
|
||||||
for i := range kinds {
|
|
||||||
kinds[i] = strings.ToLower(kinds[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
query["kind"] = bson.M{"$in": kinds}
|
|
||||||
}
|
|
||||||
|
|
||||||
posts, err := listPosts(query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(posts, func(i, j int) bool {
|
|
||||||
return posts[i].Position < posts[j].Position
|
|
||||||
})
|
|
||||||
|
|
||||||
return posts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPost creates a new post.
|
|
||||||
func (log *Log) NewPost(time time.Time, kind, nick, text string) (Post, error) {
|
|
||||||
if kind == "" || nick == "" || text == "" {
|
|
||||||
return Post{}, errors.New("Missing/empty parameters")
|
|
||||||
}
|
|
||||||
|
|
||||||
postMutex.RLock()
|
|
||||||
defer postMutex.RUnlock()
|
|
||||||
|
|
||||||
position, err := counter.Next("next_post_id", log.ShortID)
|
|
||||||
if err != nil {
|
|
||||||
return Post{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
post := Post{
|
|
||||||
ID: MakePostID(time),
|
|
||||||
Position: position,
|
|
||||||
LogID: log.ShortID,
|
|
||||||
Time: time,
|
|
||||||
Kind: kind,
|
|
||||||
Nick: nick,
|
|
||||||
Text: text,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = postCollection.Insert(post)
|
|
||||||
if err != nil {
|
|
||||||
return Post{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return post, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCharacters updates the character list
|
|
||||||
func (log *Log) UpdateCharacters() error {
|
|
||||||
characterUpdateMutex.Lock()
|
|
||||||
defer characterUpdateMutex.Unlock()
|
|
||||||
|
|
||||||
posts, err := log.Posts()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
added := make(map[string]bool)
|
|
||||||
removed := make(map[string]bool)
|
|
||||||
for _, post := range posts {
|
|
||||||
if post.Kind == "text" || post.Kind == "action" {
|
|
||||||
if strings.HasPrefix(post.Text, "(") || strings.Contains(post.Nick, "(") || strings.Contains(post.Nick, "[E]") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up the nick (remove possessive suffix, comma, formatting stuff)
|
|
||||||
if strings.HasSuffix(post.Nick, "'s") || strings.HasSuffix(post.Nick, "`s") {
|
|
||||||
post.Nick = post.Nick[:len(post.Nick)-2]
|
|
||||||
} else if strings.HasSuffix(post.Nick, "'") || strings.HasSuffix(post.Nick, "`") || strings.HasSuffix(post.Nick, ",") || strings.HasSuffix(post.Nick, "\x0f") {
|
|
||||||
post.Nick = post.Nick[:len(post.Nick)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
added[post.Nick] = true
|
|
||||||
}
|
|
||||||
if post.Kind == "chars" {
|
|
||||||
tokens := strings.Fields(post.Text)
|
|
||||||
for _, token := range tokens {
|
|
||||||
if strings.HasPrefix(token, "-") {
|
|
||||||
removed[token[1:]] = true
|
|
||||||
} else {
|
|
||||||
added[strings.Replace(token, "+", "", 1)] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nicks := make([]string, 0, len(added))
|
|
||||||
for nick := range added {
|
|
||||||
if added[nick] && !removed[nick] {
|
|
||||||
nicks = append(nicks, nick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
characters, err := character.ListNicks(nicks...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
characterIDs := make([]string, len(characters))
|
|
||||||
for i, char := range characters {
|
|
||||||
characterIDs[i] = char.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
err = logsCollection.UpdateId(log.ID, bson.M{"$set": bson.M{"characterIds": characterIDs}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, nick := range nicks {
|
|
||||||
found := false
|
|
||||||
|
|
||||||
for _, character := range characters {
|
|
||||||
if character.HasNick(nick) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
addUnknownNick(nick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.CharacterIDs = characterIDs
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findLog(query interface{}) (Log, error) {
|
|
||||||
log := Log{}
|
|
||||||
err := logsCollection.Find(query).One(&log)
|
|
||||||
if err != nil {
|
|
||||||
return Log{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return log, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listLog(query interface{}, limit int) ([]Log, error) {
|
|
||||||
logs := make([]Log, 0, 64)
|
|
||||||
err := logsCollection.Find(query).Limit(limit).Sort("-date").All(&logs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return logs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func iterLogs(query interface{}, limit int) *mgo.Iter {
|
|
||||||
return logsCollection.Find(query).Sort("-date").Limit(limit).Batch(8).Iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeLogID generates log IDs that are of the format from logbot2, though it will break compatibility.
|
|
||||||
func MakeLogID(date time.Time, channel string) string {
|
|
||||||
return fmt.Sprintf("%s%03d_%s", date.UTC().Format("2006-01-02_150405"), (date.Nanosecond() / int(time.Millisecond/time.Nanosecond)), channel[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
logsCollection = db.C("logbot3.logs")
|
|
||||||
|
|
||||||
logsCollection.EnsureIndexKey("date")
|
|
||||||
logsCollection.EnsureIndexKey("channel")
|
|
||||||
logsCollection.EnsureIndexKey("characterIds")
|
|
||||||
logsCollection.EnsureIndexKey("event")
|
|
||||||
logsCollection.EnsureIndex(mgo.Index{
|
|
||||||
Key: []string{"channel", "open"},
|
|
||||||
})
|
|
||||||
err := logsCollection.EnsureIndex(mgo.Index{
|
|
||||||
Key: []string{"shortId"},
|
|
||||||
Unique: true,
|
|
||||||
DropDups: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("init logbot3.logs:", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
package log
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var postCollection *mgo.Collection
|
|
||||||
|
|
||||||
// A Post is a part of a log file.
|
|
||||||
type Post struct {
|
|
||||||
ID string `bson:"_id"`
|
|
||||||
LogID string `bson:"logId"`
|
|
||||||
Time time.Time `bson:"time"`
|
|
||||||
Kind string `bson:"kind"`
|
|
||||||
Nick string `bson:"nick"`
|
|
||||||
Text string `bson:"text"`
|
|
||||||
Position int `bson:"position"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit the post
|
|
||||||
func (post *Post) Edit(time *time.Time, kind *string, nick *string, text *string) error {
|
|
||||||
changes := bson.M{}
|
|
||||||
changed := false
|
|
||||||
postCopy := *post
|
|
||||||
|
|
||||||
if time != nil && !time.IsZero() && !time.Equal(post.Time) {
|
|
||||||
changes["time"] = *time
|
|
||||||
changed = true
|
|
||||||
postCopy.Time = *time
|
|
||||||
}
|
|
||||||
if kind != nil && *kind != "" && *kind != post.Kind {
|
|
||||||
changes["kind"] = *kind
|
|
||||||
changed = true
|
|
||||||
postCopy.Kind = *kind
|
|
||||||
}
|
|
||||||
if nick != nil && *nick != "" && *nick != post.Nick {
|
|
||||||
changes["nick"] = *nick
|
|
||||||
changed = true
|
|
||||||
postCopy.Nick = *nick
|
|
||||||
}
|
|
||||||
if text != nil && *text != "" && *text != post.Text {
|
|
||||||
changes["text"] = *text
|
|
||||||
changed = true
|
|
||||||
postCopy.Text = *text
|
|
||||||
}
|
|
||||||
|
|
||||||
if !changed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := postCollection.UpdateId(post.ID, bson.M{"$set": changes})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*post = postCopy
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the post
|
|
||||||
func (post *Post) Move(toPosition int) error {
|
|
||||||
if toPosition < 1 {
|
|
||||||
return errors.New("Invalid position")
|
|
||||||
}
|
|
||||||
|
|
||||||
postMutex.Lock()
|
|
||||||
defer postMutex.Unlock()
|
|
||||||
|
|
||||||
// To avoid problems, only allow target indices that are allowed. If it's 1, then there is bound to
|
|
||||||
// be a post at the position.
|
|
||||||
if toPosition > 1 {
|
|
||||||
existingPost := Post{}
|
|
||||||
err := postCollection.Find(bson.M{"logId": post.LogID, "position": toPosition}).One(&existingPost)
|
|
||||||
|
|
||||||
if err != nil || existingPost.Position != toPosition {
|
|
||||||
return errors.New("No post found at the position")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query := bson.M{"logId": post.LogID}
|
|
||||||
operation := bson.M{"$inc": bson.M{"position": 1}}
|
|
||||||
|
|
||||||
if toPosition < post.Position {
|
|
||||||
query["$and"] = []bson.M{
|
|
||||||
bson.M{"position": bson.M{"$gte": toPosition}},
|
|
||||||
bson.M{"position": bson.M{"$lt": post.Position}},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query["$and"] = []bson.M{
|
|
||||||
bson.M{"position": bson.M{"$gt": post.Position}},
|
|
||||||
bson.M{"position": bson.M{"$lte": toPosition}},
|
|
||||||
}
|
|
||||||
|
|
||||||
operation["$inc"] = bson.M{"position": -1}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := postCollection.UpdateAll(query, operation)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("moving others: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = postCollection.UpdateId(post.ID, bson.M{"$set": bson.M{"position": toPosition}})
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("moving: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
post.Position = toPosition
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindPostID finds a log post by ID.
|
|
||||||
func FindPostID(id string) (Post, error) {
|
|
||||||
return findPost(bson.M{"_id": id})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPostIDs lists log posts by ID
|
|
||||||
func ListPostIDs(ids ...string) ([]Post, error) {
|
|
||||||
return listPosts(bson.M{"_id": bson.M{"$in": ids}})
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemovePost removes a post, moving all subsequent post up one position
|
|
||||||
func RemovePost(id string) (Post, error) {
|
|
||||||
postMutex.Lock()
|
|
||||||
defer postMutex.Unlock()
|
|
||||||
|
|
||||||
post, err := findPost(bson.M{"_id": id})
|
|
||||||
if err != nil {
|
|
||||||
return Post{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = postCollection.RemoveId(id)
|
|
||||||
if err != nil {
|
|
||||||
return Post{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = postCollection.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
|
|
||||||
if err != nil {
|
|
||||||
return Post{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return post, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findPost(query interface{}) (Post, error) {
|
|
||||||
post := Post{}
|
|
||||||
err := postCollection.Find(query).One(&post)
|
|
||||||
if err != nil {
|
|
||||||
return Post{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return post, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listPosts(query interface{}) ([]Post, error) {
|
|
||||||
posts := make([]Post, 0, 64)
|
|
||||||
err := postCollection.Find(query).All(&posts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return posts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakePostID makes a random post ID
|
|
||||||
func MakePostID(time time.Time) string {
|
|
||||||
data := make([]byte, 4)
|
|
||||||
rand.Read(data)
|
|
||||||
|
|
||||||
return "P" + strconv.FormatInt(time.UnixNano(), 36) + strconv.FormatInt(int64(binary.LittleEndian.Uint32(data)), 36)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
postCollection = db.C("logbot3.posts")
|
|
||||||
|
|
||||||
postCollection.EnsureIndexKey("logId")
|
|
||||||
postCollection.EnsureIndexKey("time")
|
|
||||||
postCollection.EnsureIndexKey("kind")
|
|
||||||
postCollection.EnsureIndexKey("position")
|
|
||||||
|
|
||||||
err := postCollection.EnsureIndex(mgo.Index{
|
|
||||||
Key: []string{"$text:text"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("init logbot3.logs:", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package log
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var unknownConnection *mgo.Collection
|
|
||||||
|
|
||||||
// An UnknownNick is a nick found by the character list updater that
|
|
||||||
// does not exist. The score is the number of logs that nick was in, meaning
|
|
||||||
// nicks with a higher score should be a high priority to be matched with
|
|
||||||
// a character.
|
|
||||||
type UnknownNick struct {
|
|
||||||
Nick string `bson:"_id" json:"nick"`
|
|
||||||
Score int `bson:"score" json:"score"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnknownNicks gets all the unknown nicks from the last search.
|
|
||||||
func UnknownNicks() ([]UnknownNick, error) {
|
|
||||||
nicks := make([]UnknownNick, 0, 256)
|
|
||||||
err := unknownConnection.Find(bson.M{}).Sort("-score").All(&nicks)
|
|
||||||
|
|
||||||
return nicks, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func addUnknownNick(nick string) error {
|
|
||||||
_, err := unknownConnection.UpsertId(nick, bson.M{"$inc": bson.M{"score": 1}})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearUnknownNicks() error {
|
|
||||||
_, err := unknownConnection.RemoveAll(bson.M{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
unknownConnection = db.C("logbot3.unknown_nicks")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package log
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var scheduleCharacterUpdate = func() func() {
|
|
||||||
var mutex sync.Mutex
|
|
||||||
var scheduled bool
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
mutex.Lock()
|
|
||||||
if !scheduled {
|
|
||||||
go func() {
|
|
||||||
time.Sleep(time.Second * 60)
|
|
||||||
|
|
||||||
// If another comes along in the next 2-3 seconds, it should schedule a new
|
|
||||||
// round to avoid a character only appearing in half their logs.
|
|
||||||
mutex.Lock()
|
|
||||||
scheduled = false
|
|
||||||
mutex.Unlock()
|
|
||||||
|
|
||||||
UpdateAllCharacters()
|
|
||||||
}()
|
|
||||||
|
|
||||||
scheduled = true
|
|
||||||
}
|
|
||||||
mutex.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// ScheduleCharacterUpdate schedules a full update within the minute.
|
|
||||||
// Subsequent calls within that time will not schedule anything. Even
|
|
||||||
// if the operation takes a few seconds at most, it need not be ran often.
|
|
||||||
func ScheduleCharacterUpdate() {
|
|
||||||
scheduleCharacterUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCharacters is a shorthand for getting a log and updaing its characters
|
|
||||||
func UpdateCharacters(logID string) error {
|
|
||||||
log, err := FindID(logID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return log.UpdateCharacters()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateAllCharacters updates character list on all logs. This should
|
|
||||||
// be done if one or more characters failed to be added.
|
|
||||||
func UpdateAllCharacters() (updated int, err error) {
|
|
||||||
updated = 0
|
|
||||||
|
|
||||||
err = clearUnknownNicks()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iter := iterLogs(bson.M{}, 0)
|
|
||||||
err = iter.Err()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log := Log{}
|
|
||||||
for iter.Next(&log) {
|
|
||||||
err = log.UpdateCharacters()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updated++
|
|
||||||
}
|
|
||||||
|
|
||||||
err = iter.Err()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package story
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var chapterCollection *mgo.Collection
|
|
||||||
|
|
||||||
// A Chapter is a part of a story.
|
|
||||||
type Chapter struct {
|
|
||||||
ID string `bson:"_id"`
|
|
||||||
StoryID string `bson:"storyId"`
|
|
||||||
Title string `bson:"title"`
|
|
||||||
Author string `bson:"author"`
|
|
||||||
Source string `bson:"source"`
|
|
||||||
CreatedDate time.Time `bson:"createdDate"`
|
|
||||||
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
|
|
||||||
EditedDate time.Time `bson:"editedDate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit edits a chapter, and updates EditedDate. While many Edit functions cheat if there's nothing to
|
|
||||||
// change, this functill will due to EditedDate.
|
|
||||||
func (chapter *Chapter) Edit(title, source *string, fictionalDate *time.Time) error {
|
|
||||||
now := time.Now()
|
|
||||||
changes := bson.M{"editedDate": now}
|
|
||||||
changed := *chapter
|
|
||||||
changed.EditedDate = now
|
|
||||||
|
|
||||||
if title != nil && *title != chapter.Title {
|
|
||||||
changes["title"] = *title
|
|
||||||
changed.Title = *title
|
|
||||||
}
|
|
||||||
if source != nil && *source != chapter.Source {
|
|
||||||
changes["source"] = *source
|
|
||||||
changed.Source = *source
|
|
||||||
}
|
|
||||||
if fictionalDate != nil && *fictionalDate != chapter.FictionalDate {
|
|
||||||
changes["fictionalDate"] = *fictionalDate
|
|
||||||
changed.FictionalDate = *fictionalDate
|
|
||||||
}
|
|
||||||
|
|
||||||
err := chapterCollection.UpdateId(chapter.ID, bson.M{"$set": changes})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*chapter = changed
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes a chapter.
|
|
||||||
func (chapter *Chapter) Remove() error {
|
|
||||||
return chapterCollection.RemoveId(chapter.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeChapterID makes a random chapter ID that's 24 characters long
|
|
||||||
func makeChapterID() string {
|
|
||||||
result := "SC"
|
|
||||||
offset := 0
|
|
||||||
data := make([]byte, 32)
|
|
||||||
|
|
||||||
rand.Read(data)
|
|
||||||
for len(result) < 24 {
|
|
||||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
|
||||||
offset += 8
|
|
||||||
|
|
||||||
if offset >= 32 {
|
|
||||||
rand.Read(data)
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result[:24]
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
chapterCollection = db.C("story.chapters")
|
|
||||||
|
|
||||||
chapterCollection.EnsureIndexKey("storyId")
|
|
||||||
chapterCollection.EnsureIndexKey("author")
|
|
||||||
chapterCollection.EnsureIndexKey("createdDate")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
package story
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
|
||||||
"github.com/globalsign/mgo"
|
|
||||||
"github.com/globalsign/mgo/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
var storyCollection *mgo.Collection
|
|
||||||
|
|
||||||
// ErrTagAlreadyExists is an error returned by Story.AddTag
|
|
||||||
var ErrTagAlreadyExists = errors.New("Tag already exists")
|
|
||||||
|
|
||||||
// ErrTagNotExists is an error returned by Story.RemoveTag
|
|
||||||
var ErrTagNotExists = errors.New("Tag does not exist")
|
|
||||||
|
|
||||||
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
|
|
||||||
// The story model is a container for multiple chapters this time, in contrast to the previous version.
|
|
||||||
type Story struct {
|
|
||||||
ID string `bson:"_id"`
|
|
||||||
Author string `bson:"author"`
|
|
||||||
Name string `bson:"name"`
|
|
||||||
Category string `bson:"category"`
|
|
||||||
Open bool `bson:"open"`
|
|
||||||
Listed bool `bson:"listed"`
|
|
||||||
Tags []Tag `bson:"tags"`
|
|
||||||
CreatedDate time.Time `bson:"createdDate"`
|
|
||||||
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
|
|
||||||
UpdatedDate time.Time `bson:"updatedDate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTag adds a tag to the story. It returns ErrTagAlreadyExists if the tag is already there
|
|
||||||
func (story *Story) AddTag(tag Tag) error {
|
|
||||||
for i := range story.Tags {
|
|
||||||
if story.Tags[i].Equal(tag) {
|
|
||||||
return ErrTagAlreadyExists
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := storyCollection.UpdateId(story.ID, bson.M{"$push": bson.M{"tags": tag}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
story.Tags = append(story.Tags, tag)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveTag removes a tag to the story. It returns ErrTagNotExists if the tag does not exist.
|
|
||||||
func (story *Story) RemoveTag(tag Tag) error {
|
|
||||||
index := -1
|
|
||||||
for i := range story.Tags {
|
|
||||||
if story.Tags[i].Equal(tag) {
|
|
||||||
index = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if index == -1 {
|
|
||||||
return ErrTagNotExists
|
|
||||||
}
|
|
||||||
|
|
||||||
err := storyCollection.UpdateId(story.ID, bson.M{"$pull": bson.M{"tags": tag}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
story.Tags = append(story.Tags[:index], story.Tags[index+1:]...)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edit edits the story, reflecting the new values in the story's struct values. If nothing will be
|
|
||||||
// changed, it will silently return without a database roundtrip.
|
|
||||||
func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDate *time.Time) error {
|
|
||||||
changes := bson.M{}
|
|
||||||
changed := *story
|
|
||||||
|
|
||||||
if name != nil && *name == story.Name {
|
|
||||||
changes["name"] = *name
|
|
||||||
changed.Name = *name
|
|
||||||
}
|
|
||||||
if category != nil && *category == story.Category {
|
|
||||||
changes["category"] = *category
|
|
||||||
changed.Name = *category
|
|
||||||
}
|
|
||||||
if listed != nil && *listed == story.Listed {
|
|
||||||
changes["listed"] = *listed
|
|
||||||
changed.Listed = *listed
|
|
||||||
}
|
|
||||||
if open != nil && *open == story.Open {
|
|
||||||
changes["open"] = *open
|
|
||||||
changed.Open = *open
|
|
||||||
}
|
|
||||||
if fictionalDate != nil && *fictionalDate == story.FictionalDate {
|
|
||||||
changes["fictionalDate"] = *fictionalDate
|
|
||||||
changed.FictionalDate = *fictionalDate
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(changes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := storyCollection.UpdateId(story.ID, bson.M{"$set": changes})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*story = changed
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the story from the database
|
|
||||||
func (story *Story) Remove() error {
|
|
||||||
return storyCollection.RemoveId(story.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
|
|
||||||
// occurs
|
|
||||||
func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) {
|
|
||||||
if !story.Open && author != story.Author {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARNING: AddChapter is breaking Open rules (story.id=%#+v, story.name=%#+v, chapter.author=%#+v, chapter.title=%#+v)", story.ID, story.Name, author, title)
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter := Chapter{
|
|
||||||
ID: makeChapterID(),
|
|
||||||
StoryID: story.ID,
|
|
||||||
Title: title,
|
|
||||||
Author: author,
|
|
||||||
Source: source,
|
|
||||||
CreatedDate: createdDate,
|
|
||||||
FictionalDate: finctionalDate,
|
|
||||||
EditedDate: createdDate,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := chapterCollection.Insert(chapter)
|
|
||||||
if err != nil {
|
|
||||||
return Chapter{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if createdDate.After(story.UpdatedDate) {
|
|
||||||
if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil {
|
|
||||||
story.UpdatedDate = createdDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new story.
|
|
||||||
func New(name, author, category string, listed, open bool, tags []Tag, createdDate, fictionalDate time.Time) (Story, error) {
|
|
||||||
story := Story{
|
|
||||||
ID: makeStoryID(),
|
|
||||||
Name: name,
|
|
||||||
Author: author,
|
|
||||||
Category: category,
|
|
||||||
Listed: listed,
|
|
||||||
Open: open,
|
|
||||||
Tags: tags,
|
|
||||||
CreatedDate: createdDate,
|
|
||||||
FictionalDate: fictionalDate,
|
|
||||||
UpdatedDate: createdDate,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := storyCollection.Insert(story)
|
|
||||||
if err != nil {
|
|
||||||
return Story{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return story, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindID finds a story by ID
|
|
||||||
func FindID(id string) (Story, error) {
|
|
||||||
story := Story{}
|
|
||||||
err := storyCollection.FindId(id).One(&story)
|
|
||||||
|
|
||||||
return story, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// List lists stories by any non-zero criteria passed with it.
|
|
||||||
func List(author string, tags []Tag, earliest, latest time.Time, limit int) ([]Story, error) {
|
|
||||||
query := bson.M{}
|
|
||||||
|
|
||||||
if author != "" {
|
|
||||||
query["author"] = author
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tags) > 0 {
|
|
||||||
query["tags"] = bson.M{"$in": tags}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !earliest.IsZero() && !latest.IsZero() {
|
|
||||||
query["fictionalDate"] = bson.M{
|
|
||||||
"$gte": earliest,
|
|
||||||
"$lt": latest,
|
|
||||||
}
|
|
||||||
} else if !latest.IsZero() {
|
|
||||||
query["fictionalDate"] = bson.M{
|
|
||||||
"$lt": latest,
|
|
||||||
}
|
|
||||||
} else if !earliest.IsZero() {
|
|
||||||
query["fictionalDate"] = bson.M{
|
|
||||||
"$gte": earliest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stories := make([]Story, 0, 128)
|
|
||||||
err := storyCollection.Find(query).Limit(limit).One(&stories)
|
|
||||||
|
|
||||||
return stories, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeStoryID makes a random story ID that's 16 characters long
|
|
||||||
func makeStoryID() string {
|
|
||||||
result := "S"
|
|
||||||
offset := 0
|
|
||||||
data := make([]byte, 32)
|
|
||||||
|
|
||||||
rand.Read(data)
|
|
||||||
for len(result) < 16 {
|
|
||||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
|
||||||
offset += 8
|
|
||||||
|
|
||||||
if offset >= 32 {
|
|
||||||
rand.Read(data)
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result[:16]
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
|
||||||
storyCollection = db.C("story.stories")
|
|
||||||
|
|
||||||
storyCollection.EnsureIndexKey("tags")
|
|
||||||
storyCollection.EnsureIndexKey("author")
|
|
||||||
storyCollection.EnsureIndexKey("updatedDate")
|
|
||||||
storyCollection.EnsureIndexKey("fictionalDate")
|
|
||||||
storyCollection.EnsureIndexKey("listed")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package story
|
|
||||||
|
|
||||||
// A Tag associates a story with other content, like other stories, logs and more.
|
|
||||||
type Tag struct {
|
|
||||||
Kind string `bson:"kind"`
|
|
||||||
Name string `bson:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equal returns true if the tags match one another.
|
|
||||||
func (tag *Tag) Equal(other Tag) bool {
|
|
||||||
return tag.Kind == other.Kind && tag.Name == other.Name
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChangeModel describes a model related to the change.
|
||||||
|
type ChangeModel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ChangeModelCharacter is a value of ChangeModel
|
||||||
|
ChangeModelCharacter ChangeModel = "Character"
|
||||||
|
// ChangeModelChannel is a value of ChangeModel
|
||||||
|
ChangeModelChannel ChangeModel = "Channel"
|
||||||
|
// ChangeModelLog is a value of ChangeModel
|
||||||
|
ChangeModelLog ChangeModel = "Log"
|
||||||
|
// ChangeModelPost is a value of ChangeModel
|
||||||
|
ChangeModelPost ChangeModel = "Post"
|
||||||
|
// ChangeModelStory is a value of ChangeModel
|
||||||
|
ChangeModelStory ChangeModel = "Story"
|
||||||
|
// ChangeModelTag is a value of ChangeModel
|
||||||
|
ChangeModelTag ChangeModel = "Tag"
|
||||||
|
// ChangeModelChapter is a value of ChangeModel
|
||||||
|
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.
|
||||||
|
func (e ChangeModel) IsValid() bool {
|
||||||
|
switch e {
|
||||||
|
case ChangeModelCharacter, ChangeModelChannel, ChangeModelLog, ChangeModelPost, ChangeModelStory, ChangeModelTag, ChangeModelChapter, ChangeModelComment:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ChangeModel) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalGQL unmarshals the underlying graphql value.
|
||||||
|
func (e *ChangeModel) UnmarshalGQL(v interface{}) error {
|
||||||
|
str, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enums must be strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = ChangeModel(str)
|
||||||
|
if !e.IsValid() {
|
||||||
|
return fmt.Errorf("%s is not a valid ChangeModel", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalGQL marshals the underlying graphql value.
|
||||||
|
func (e ChangeModel) MarshalGQL(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user