Compare commits
208
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0b6e6b1e9f |
+4
-4
@@ -6,16 +6,16 @@ pipeline:
|
||||
docker-release:
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata
|
||||
tags: latest
|
||||
auto_tag: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
when:
|
||||
event: tag
|
||||
ref: refs/tags/*
|
||||
docker-dev:
|
||||
docker-release-latest:
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata
|
||||
tags: trunk
|
||||
tags: latest
|
||||
secrets: [ docker_username, docker_password ]
|
||||
when:
|
||||
event: push
|
||||
event: tag
|
||||
ref: refs/tags/*
|
||||
+11
-2
@@ -1,6 +1,15 @@
|
||||
vendor
|
||||
schema/bindata.go
|
||||
graphql/schema/bindata.go
|
||||
config.json
|
||||
api
|
||||
debug
|
||||
build
|
||||
build
|
||||
rpdata-graphiql
|
||||
rpdata-*
|
||||
!rpdata-*/
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
/graph2/graphcore/*_gen.go
|
||||
generated.gql
|
||||
generated.go
|
||||
|
||||
+18
-17
@@ -1,26 +1,27 @@
|
||||
# Use Go 1.10
|
||||
FROM golang:1.10
|
||||
## 1. Build Server
|
||||
# Use Go 1.12
|
||||
FROM golang:1.12 AS build-server
|
||||
|
||||
# Load repository into docker
|
||||
WORKDIR /go/src/git.aiterp.net/rpdata/api/
|
||||
WORKDIR /project/
|
||||
COPY . .
|
||||
|
||||
# Load 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
|
||||
# Install build tools and dependencies
|
||||
RUN go generate ./...
|
||||
|
||||
# Build server and tools (CGO disabled to make them 100% static)
|
||||
ENV CGO_ENABLED 0
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-graphiql ./cmd/rpdata-graphiql
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2charimport ./cmd/rpdata-lb2charimport
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2logimport ./cmd/rpdata-lb2logimport
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-wikifileimport ./cmd/rpdata-wikifileimport
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-ensurechannels ./cmd/rpdata-ensurechannels
|
||||
RUN go build -a -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-as2storyimport ./cmd/rpdata-as2storyimport
|
||||
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-server ./cmd/rpdata-server
|
||||
|
||||
# Move artifacts into final container
|
||||
FROM alpine:latest
|
||||
COPY --from=0 /binaries/* /usr/local/bin/
|
||||
CMD ["/usr/local/bin/rpdata-graphiql"]
|
||||
## 2. Distribute
|
||||
# Use alpine linux
|
||||
FROM alpine:3.7
|
||||
|
||||
# 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
|
||||
|
||||
Project RPData aims to centralize the AiteRP applications, and partially involve the wiki, into a common API. This will allow for easy linking and inlining between them. The primary consumers of this API will be the Logbot and the upcoming RPData website.
|
||||
Project RPData aims to centralize the AiteRP applications, and partially involve the wiki into a common API. This will allow for easy linking and inlining between them. The primary consumers of this API will be the Logbot and the upcoming RPData website.
|
||||
|
||||
## Progress
|
||||
|
||||
### Complete
|
||||
* Session
|
||||
* Character
|
||||
* Log
|
||||
* Post
|
||||
* File
|
||||
|
||||
### Remaining
|
||||
* Story
|
||||
|
||||
### Future
|
||||
* User (manual DB edits will suffice for now)
|
||||
There is some renovation going on, and the packages `model` and `graphql` are going to be superceded by `models` and `graph2`
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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,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,109 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type changeRepository struct {
|
||||
changes *mgo.Collection
|
||||
idCounter *counter
|
||||
}
|
||||
|
||||
func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) {
|
||||
change := new(models.Change)
|
||||
err := r.changes.FindId(id).One(change)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return change, nil
|
||||
}
|
||||
|
||||
func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) {
|
||||
query := bson.M{}
|
||||
limit := 0
|
||||
if filter.EarliestDate != nil && !filter.EarliestDate.IsZero() {
|
||||
query["date"] = bson.M{"$gte": *filter.EarliestDate}
|
||||
}
|
||||
if len(filter.Keys) > 0 {
|
||||
query["keys"] = bson.M{"$in": filter.Keys}
|
||||
} else {
|
||||
query["listed"] = true
|
||||
}
|
||||
if filter.Author != nil && *filter.Author != "" {
|
||||
query["author"] = *filter.Author
|
||||
}
|
||||
if filter.Limit != nil {
|
||||
limit = *filter.Limit
|
||||
}
|
||||
|
||||
initialSize := 64
|
||||
if limit > 0 && limit < 256 {
|
||||
initialSize = limit
|
||||
}
|
||||
|
||||
changes := make([]*models.Change, 0, initialSize)
|
||||
err := r.changes.Find(query).Sort("-date").Limit(limit).All(&changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) {
|
||||
next, err := r.idCounter.Increment(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
change.ID = "Change_" + strconv.Itoa(next)
|
||||
|
||||
err = r.changes.Insert(&change)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &change, nil
|
||||
}
|
||||
|
||||
func (r *changeRepository) Remove(ctx context.Context, change models.Change) error {
|
||||
return r.changes.RemoveId(change.ID)
|
||||
}
|
||||
|
||||
func newChangeRepository(db *mgo.Database) (repositories.ChangeRepository, error) {
|
||||
collection := db.C("common.changes")
|
||||
|
||||
// Delete the old index if it exists.
|
||||
_ = collection.DropIndexName("date_1")
|
||||
|
||||
err := collection.EnsureIndex(mgo.Index{
|
||||
Name: "expiry",
|
||||
Key: []string{"date"},
|
||||
ExpireAfter: time.Hour * 2400, // 100 days
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndexKey("keys")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &changeRepository{
|
||||
changes: collection,
|
||||
idCounter: newCounter(db, "auto_increment", "Change"),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
func newChannelRepository(db *mgo.Database) (*channelRepository, error) {
|
||||
collection := db.C("common.channels")
|
||||
|
||||
err := collection.EnsureIndexKey("logged")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("event")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("location")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &channelRepository{
|
||||
channels: collection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type channelRepository struct {
|
||||
channels *mgo.Collection
|
||||
}
|
||||
|
||||
func (r *channelRepository) Find(ctx context.Context, name string) (*models.Channel, error) {
|
||||
channel := new(models.Channel)
|
||||
err := r.channels.FindId(name).One(channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (r *channelRepository) List(ctx context.Context, filter models.ChannelFilter) ([]*models.Channel, error) {
|
||||
query := bson.M{}
|
||||
if filter.Logged != nil {
|
||||
query["logged"] = *filter.Logged
|
||||
}
|
||||
if filter.EventName != nil {
|
||||
query["eventName"] = *filter.EventName
|
||||
}
|
||||
if filter.LocationName != nil {
|
||||
query["locationName"] = *filter.LocationName
|
||||
}
|
||||
if len(filter.Names) > 0 {
|
||||
query["_id"] = bson.M{"$in": filter.Names}
|
||||
}
|
||||
|
||||
channels := make([]*models.Channel, 0, 32)
|
||||
err := r.channels.Find(query).Limit(filter.Limit).Sort("_id").All(&channels)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (r *channelRepository) Insert(ctx context.Context, channel models.Channel) (*models.Channel, error) {
|
||||
err := r.channels.Insert(channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *channelRepository) Update(ctx context.Context, channel models.Channel, update models.ChannelUpdate) (*models.Channel, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Logged != nil {
|
||||
updateBson["logged"] = *update.Logged
|
||||
channel.Logged = *update.Logged
|
||||
}
|
||||
if update.LocationName != nil {
|
||||
updateBson["locationName"] = *update.LocationName
|
||||
channel.LocationName = *update.LocationName
|
||||
}
|
||||
if update.EventName != nil {
|
||||
updateBson["eventName"] = *update.EventName
|
||||
channel.EventName = *update.EventName
|
||||
}
|
||||
|
||||
err := r.channels.UpdateId(channel.Name, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *channelRepository) Remove(ctx context.Context, channel models.Channel) error {
|
||||
return r.channels.RemoveId(channel.Name)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"log"
|
||||
)
|
||||
|
||||
type chapterRepository struct {
|
||||
chapters *mgo.Collection
|
||||
comments *mgo.Collection
|
||||
}
|
||||
|
||||
func newChapterRepository(db *mgo.Database) (repositories.ChapterRepository, error) {
|
||||
collection := db.C("story.chapters")
|
||||
|
||||
err := collection.EnsureIndexKey("storyId")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("createdDate")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chapterRepository{
|
||||
chapters: collection,
|
||||
comments: db.C("story.comments"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Find(ctx context.Context, id string) (*models.Chapter, error) {
|
||||
chapter := new(models.Chapter)
|
||||
err := r.chapters.FindId(id).One(chapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chapter, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) {
|
||||
query := bson.M{}
|
||||
if filter.StoryID != nil {
|
||||
query["storyId"] = *filter.StoryID
|
||||
}
|
||||
|
||||
chapters := make([]*models.Chapter, 0, 32)
|
||||
err := r.chapters.Find(query).Sort("createdDate").Limit(filter.Limit).All(&chapters)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return chapters, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chapters, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) {
|
||||
chapter.ID = generate.StoryID()
|
||||
|
||||
err := r.chapters.Insert(chapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Title != nil {
|
||||
updateBson["title"] = *update.Title
|
||||
chapter.Title = *update.Title
|
||||
}
|
||||
if update.Source != nil {
|
||||
updateBson["source"] = *update.Source
|
||||
chapter.Source = *update.Source
|
||||
}
|
||||
if update.FictionalDate != nil {
|
||||
updateBson["fictionalDate"] = *update.FictionalDate
|
||||
chapter.FictionalDate = *update.FictionalDate
|
||||
}
|
||||
if update.CommentMode != nil {
|
||||
updateBson["commentMode"] = *update.CommentMode
|
||||
chapter.CommentMode = *update.CommentMode
|
||||
}
|
||||
if update.CommentsLocked != nil {
|
||||
updateBson["commentsLocked"] = *update.CommentsLocked
|
||||
chapter.CommentsLocked = *update.CommentsLocked
|
||||
}
|
||||
|
||||
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Move(ctx context.Context, chapter models.Chapter, from, to models.Story) (*models.Chapter, error) {
|
||||
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": bson.M{"storyId": to.ID}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chapter.StoryID = to.ID
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Delete(ctx context.Context, chapter models.Chapter) error {
|
||||
err := r.chapters.RemoveId(chapter.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := r.comments.RemoveAll(bson.M{"chapterId": chapter.ID})
|
||||
if err != nil {
|
||||
log.Println("Failed to remove comments:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Removed chapter %s (%d comments)", chapter.ID, c.Removed)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
)
|
||||
|
||||
type characterRepository struct {
|
||||
characters *mgo.Collection
|
||||
cidCounter *counter
|
||||
}
|
||||
|
||||
func newCharacterRepository(db *mgo.Database) (repositories.CharacterRepository, error) {
|
||||
collection := db.C("common.characters")
|
||||
|
||||
err := collection.EnsureIndexKey("name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndexKey("shortName")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"nicks"},
|
||||
Unique: true,
|
||||
DropDups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"$text:description"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &characterRepository{
|
||||
characters: collection,
|
||||
cidCounter: newCounter(db, "auto_increment", "Character"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) Find(ctx context.Context, id string) (*models.Character, error) {
|
||||
character := new(models.Character)
|
||||
err := r.characters.FindId(id).One(character)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) FindNick(ctx context.Context, nick string) (*models.Character, error) {
|
||||
character := new(models.Character)
|
||||
err := r.characters.Find(bson.M{"nick": nick}).One(character)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) FindName(ctx context.Context, name string) (*models.Character, error) {
|
||||
query := bson.M{
|
||||
"$or": []bson.M{
|
||||
{"shortName": name},
|
||||
{"name": name},
|
||||
},
|
||||
}
|
||||
|
||||
// Look for all characters matching query
|
||||
characters := make([]*models.Character, 0, 8)
|
||||
err := r.characters.Find(query).All(&characters)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
} else if len(characters) == 0 {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
// Prioritize exact match
|
||||
for _, character := range characters {
|
||||
if character.Name == name {
|
||||
return character, nil
|
||||
}
|
||||
}
|
||||
|
||||
return characters[0], nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) List(ctx context.Context, filter models.CharacterFilter) ([]*models.Character, error) {
|
||||
query := bson.M{}
|
||||
if filter.Author != nil {
|
||||
query["author"] = *filter.Author
|
||||
}
|
||||
if len(filter.IDs) > 0 {
|
||||
query["_id"] = bson.M{"$in": filter.IDs}
|
||||
}
|
||||
if len(filter.Nicks) > 0 {
|
||||
query["nicks"] = bson.M{"$in": filter.Nicks}
|
||||
}
|
||||
if len(filter.Names) > 0 {
|
||||
query["$or"] = []bson.M{
|
||||
{"name": bson.M{"$in": filter.Names}},
|
||||
{"shortName": bson.M{"$in": filter.Names}},
|
||||
}
|
||||
}
|
||||
if filter.Search != nil {
|
||||
query["$text"] = bson.M{"$search": *filter.Search}
|
||||
}
|
||||
|
||||
characters := make([]*models.Character, 0, 32)
|
||||
err := r.characters.Find(query).Limit(filter.Limit).All(&characters)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return characters, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(characters, func(i, j int) bool {
|
||||
ni, _ := strconv.Atoi(characters[i].ID[1:])
|
||||
nj, _ := strconv.Atoi(characters[j].ID[1:])
|
||||
|
||||
return ni < nj
|
||||
})
|
||||
|
||||
return characters, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) Insert(ctx context.Context, character models.Character) (*models.Character, error) {
|
||||
nextId, err := r.cidCounter.Increment(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
character.ID = "C" + strconv.Itoa(nextId)
|
||||
|
||||
err = r.characters.Insert(&character)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) Update(ctx context.Context, character models.Character, update models.CharacterUpdate) (*models.Character, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Name != nil {
|
||||
updateBson["name"] = *update.Name
|
||||
character.Name = *update.Name
|
||||
}
|
||||
if update.ShortName != nil {
|
||||
updateBson["shortName"] = *update.ShortName
|
||||
character.ShortName = *update.ShortName
|
||||
}
|
||||
if update.Description != nil {
|
||||
updateBson["description"] = *update.Description
|
||||
character.Description = *update.Description
|
||||
}
|
||||
|
||||
err := r.characters.UpdateId(character.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) AddNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
|
||||
if character.HasNick(nick) {
|
||||
return nil, errors.New("nick already exist")
|
||||
}
|
||||
|
||||
match := bson.M{
|
||||
"_id": character.ID,
|
||||
"nicks": bson.M{"$ne": nick},
|
||||
}
|
||||
|
||||
err := r.characters.Update(match, bson.M{"$push": bson.M{"nicks": nick}})
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
|
||||
copy(newNicks, character.Nicks)
|
||||
character.Nicks = append(newNicks, nick)
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) RemoveNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
|
||||
if !character.HasNick(nick) {
|
||||
return nil, errors.New("nick does not exist")
|
||||
}
|
||||
|
||||
match := bson.M{
|
||||
"_id": character.ID,
|
||||
"nicks": nick,
|
||||
}
|
||||
|
||||
err := r.characters.Update(match, bson.M{"$pull": bson.M{"nicks": nick}})
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
} else if mErr, ok := err.(*mgo.LastError); ok && mErr.Code == 11000 {
|
||||
return nil, errors.New("The nick belongs to another character already")
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
|
||||
copy(newNicks, character.Nicks)
|
||||
for i := range newNicks {
|
||||
if newNicks[i] == nick {
|
||||
newNicks = append(newNicks[:i], newNicks[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
character.Nicks = newNicks
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) Delete(ctx context.Context, character models.Character) error {
|
||||
return r.characters.RemoveId(character.ID)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"log"
|
||||
)
|
||||
|
||||
type commentRepository struct {
|
||||
comments *mgo.Collection
|
||||
}
|
||||
|
||||
func newCommentRepository(db *mgo.Database) (repositories.CommentRepository, error) {
|
||||
collection := db.C("story.comments")
|
||||
|
||||
err := collection.EnsureIndexKey("chapterId")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("createdDate")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &commentRepository{
|
||||
comments: collection,
|
||||
}
|
||||
|
||||
go r.fixFieldTypo()
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) Find(ctx context.Context, id string) (*models.Comment, error) {
|
||||
comment := new(models.Comment)
|
||||
err := r.comments.FindId(id).One(comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) {
|
||||
query := bson.M{}
|
||||
if filter.ChapterID != nil {
|
||||
query["chapterId"] = *filter.ChapterID
|
||||
}
|
||||
|
||||
comments := make([]*models.Comment, 0, 32)
|
||||
err := r.comments.Find(query).Sort("createdDate").Limit(filter.Limit).All(&comments)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) {
|
||||
comment.ID = generate.CommentID()
|
||||
|
||||
err := r.comments.Insert(comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Comment, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Subject != nil {
|
||||
updateBson["subject"] = *update.Subject
|
||||
comment.Subject = *update.Subject
|
||||
}
|
||||
if update.Source != nil {
|
||||
updateBson["source"] = *update.Source
|
||||
comment.Source = *update.Source
|
||||
}
|
||||
if update.FictionalDate != nil {
|
||||
updateBson["fictionalDate"] = *update.FictionalDate
|
||||
comment.FictionalDate = *update.FictionalDate
|
||||
}
|
||||
if update.CharacterID != nil {
|
||||
updateBson["characterId"] = *update.CharacterID
|
||||
comment.CharacterID = *update.CharacterID
|
||||
}
|
||||
if update.CharacterName != nil {
|
||||
updateBson["characterName"] = *update.CharacterName
|
||||
comment.CharacterName = *update.CharacterName
|
||||
}
|
||||
|
||||
err := r.comments.UpdateId(comment.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) Delete(ctx context.Context, comment models.Comment) error {
|
||||
return r.comments.RemoveId(comment.ID)
|
||||
}
|
||||
|
||||
func (r *commentRepository) fixFieldTypo() {
|
||||
c, err := r.comments.UpdateAll(bson.M{
|
||||
"editeddDate": bson.M{"$ne": nil},
|
||||
}, bson.M{
|
||||
"$rename": bson.M{"editeddDate": "editedDate"},
|
||||
})
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Failed to run name typo fix:", err)
|
||||
return
|
||||
}
|
||||
if c.Updated > 0 {
|
||||
log.Println("Fixed editeddDate field name typo in", c.Updated, "comments")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/config"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
type MongoDB struct {
|
||||
session *mgo.Session
|
||||
|
||||
changes repositories.ChangeRepository
|
||||
channels *channelRepository
|
||||
characters repositories.CharacterRepository
|
||||
tags repositories.TagRepository
|
||||
logs *logRepository
|
||||
posts *postRepository
|
||||
files *fileRepository
|
||||
stories repositories.StoryRepository
|
||||
chapters repositories.ChapterRepository
|
||||
comments repositories.CommentRepository
|
||||
keys repositories.KeyRepository
|
||||
users repositories.UserRepository
|
||||
}
|
||||
|
||||
func (m *MongoDB) Changes() repositories.ChangeRepository {
|
||||
return m.changes
|
||||
}
|
||||
|
||||
func (m *MongoDB) Characters() repositories.CharacterRepository {
|
||||
return m.characters
|
||||
}
|
||||
|
||||
func (m *MongoDB) Tags() repositories.TagRepository {
|
||||
return m.tags
|
||||
}
|
||||
|
||||
func (m *MongoDB) Logs() repositories.LogRepository {
|
||||
return m.logs
|
||||
}
|
||||
|
||||
func (m *MongoDB) Posts() repositories.PostRepository {
|
||||
return m.posts
|
||||
}
|
||||
|
||||
func (m *MongoDB) Channels() repositories.ChannelRepository {
|
||||
return m.channels
|
||||
}
|
||||
|
||||
func (m *MongoDB) Files() repositories.FileRepository {
|
||||
return m.files
|
||||
}
|
||||
|
||||
func (m *MongoDB) Stories() repositories.StoryRepository {
|
||||
return m.stories
|
||||
}
|
||||
|
||||
func (m *MongoDB) Chapters() repositories.ChapterRepository {
|
||||
return m.chapters
|
||||
}
|
||||
|
||||
func (m *MongoDB) Comments() repositories.CommentRepository {
|
||||
return m.comments
|
||||
}
|
||||
|
||||
func (m *MongoDB) Keys() repositories.KeyRepository {
|
||||
return m.keys
|
||||
}
|
||||
|
||||
func (m *MongoDB) Users() repositories.UserRepository {
|
||||
return m.users
|
||||
}
|
||||
|
||||
func (m *MongoDB) Close(ctx context.Context) error {
|
||||
m.session.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initializes the mongodb database
|
||||
func Init(cfg config.Database) (*MongoDB, error) {
|
||||
port := cfg.Port
|
||||
if port <= 0 {
|
||||
port = 27017
|
||||
}
|
||||
|
||||
session, err := mgo.DialWithInfo(&mgo.DialInfo{
|
||||
Addrs: []string{fmt.Sprintf("%s:%d", cfg.Host, port)},
|
||||
Timeout: 30 * time.Second,
|
||||
Database: cfg.Db,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
Mechanism: cfg.Mechanism,
|
||||
Source: cfg.Db,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := session.DB(cfg.Db)
|
||||
|
||||
characters, err := newCharacterRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels, err := newChannelRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changes, err := newChangeRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs, err := newLogRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
posts, err := newPostRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := newFileRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stories, err := newStoryRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chapters, err := newChapterRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comments, err := newCommentRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys, err := newKeyRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users, err := newUserRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go posts.fixPositions(logs)
|
||||
|
||||
return &MongoDB{
|
||||
session: session,
|
||||
|
||||
changes: changes,
|
||||
characters: characters,
|
||||
channels: channels,
|
||||
tags: newTagRepository(db),
|
||||
stories: stories,
|
||||
chapters: chapters,
|
||||
comments: comments,
|
||||
logs: logs,
|
||||
posts: posts,
|
||||
files: files,
|
||||
keys: keys,
|
||||
users: users,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type counter struct {
|
||||
coll *mgo.Collection
|
||||
category string
|
||||
name string
|
||||
}
|
||||
|
||||
func newCounter(db *mgo.Database, category, name string) *counter {
|
||||
return &counter{
|
||||
coll: db.C("core.counters"),
|
||||
category: category,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *counter) WithName(name string) *counter {
|
||||
return &counter{
|
||||
coll: c.coll,
|
||||
category: c.category,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *counter) WithCategory(category string) *counter {
|
||||
return &counter{
|
||||
coll: c.coll,
|
||||
category: category,
|
||||
name: c.name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *counter) With(category, name string) *counter {
|
||||
return &counter{
|
||||
coll: c.coll,
|
||||
category: category,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *counter) Increment(amount int) (int, error) {
|
||||
type counterDoc struct {
|
||||
ID string `bson:"_id"`
|
||||
Value int `bson:"value"`
|
||||
}
|
||||
|
||||
id := c.category + "." + c.name
|
||||
doc := counterDoc{}
|
||||
|
||||
_, err := c.coll.Find(bson.M{"_id": id}).Apply(mgo.Change{
|
||||
Update: bson.M{"$inc": bson.M{"value": amount}},
|
||||
Upsert: true,
|
||||
ReturnNew: true,
|
||||
}, &doc)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return doc.Value, nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
type fileRepository struct {
|
||||
files *mgo.Collection
|
||||
}
|
||||
|
||||
func (r *fileRepository) Find(ctx context.Context, id string) (*models.File, error) {
|
||||
file := new(models.File)
|
||||
err := r.files.FindId(id).One(file)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) {
|
||||
query := bson.M{}
|
||||
if filter.Author != nil {
|
||||
query["author"] = *filter.Author
|
||||
}
|
||||
if filter.MimeTypes != nil {
|
||||
query["mimeType"] = bson.M{"$in": filter.MimeTypes}
|
||||
}
|
||||
if filter.Public != nil {
|
||||
query["public"] = *filter.Public
|
||||
}
|
||||
|
||||
files := make([]*models.File, 0, 16)
|
||||
err := r.files.Find(query).Sort("-date").All(&files)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) Insert(ctx context.Context, file models.File) (*models.File, error) {
|
||||
file.ID = generate.FileID()
|
||||
|
||||
err := r.files.Insert(&file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) Update(ctx context.Context, file models.File, update models.FileUpdate) (*models.File, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Public != nil {
|
||||
updateBson["public"] = *update.Public
|
||||
file.Public = *update.Public
|
||||
}
|
||||
if update.Name != nil {
|
||||
updateBson["name"] = *update.Name
|
||||
file.Name = *update.Name
|
||||
}
|
||||
|
||||
err := r.files.UpdateId(file.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) Delete(ctx context.Context, file models.File) error {
|
||||
return r.files.RemoveId(file.ID)
|
||||
}
|
||||
|
||||
func newFileRepository(db *mgo.Database) (*fileRepository, error) {
|
||||
files := db.C("file.headers")
|
||||
|
||||
err := files.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = files.EnsureIndexKey("mimeType")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = files.EnsureIndexKey("public")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &fileRepository{files: files}, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
type keyRepository struct {
|
||||
keys *mgo.Collection
|
||||
}
|
||||
|
||||
func newKeyRepository(db *mgo.Database) (repositories.KeyRepository, error) {
|
||||
collection := db.C("auth.keys")
|
||||
|
||||
err := collection.EnsureIndexKey("user")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &keyRepository{keys: collection}, nil
|
||||
}
|
||||
|
||||
func (r *keyRepository) Find(ctx context.Context, id string) (*models.Key, error) {
|
||||
key := models.Key{}
|
||||
err := r.keys.FindId(id).One(&key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &key, err
|
||||
}
|
||||
|
||||
func (r *keyRepository) List(ctx context.Context, filter models.KeyFilter) ([]*models.Key, error) {
|
||||
query := bson.M{}
|
||||
if filter.UserID != nil {
|
||||
query["user"] = *filter.UserID
|
||||
}
|
||||
|
||||
keys := make([]*models.Key, 0, 4)
|
||||
err := r.keys.Find(query).All(&keys)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (r *keyRepository) Insert(ctx context.Context, key models.Key) (*models.Key, error) {
|
||||
key.ID = generate.KeyID()
|
||||
|
||||
err := r.keys.Insert(&key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func (r *keyRepository) Delete(ctx context.Context, key models.Key) error {
|
||||
return r.keys.RemoveId(key.ID)
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type logRepository struct {
|
||||
openMutex sync.Mutex
|
||||
|
||||
logs *mgo.Collection
|
||||
posts *mgo.Collection
|
||||
shortIdCounter *counter
|
||||
}
|
||||
|
||||
func newLogRepository(db *mgo.Database) (*logRepository, error) {
|
||||
logs := db.C("logbot3.logs")
|
||||
posts := db.C("logbot3.posts")
|
||||
|
||||
err := logs.EnsureIndexKey("date")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = logs.EnsureIndexKey("channel")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = logs.EnsureIndexKey("characterIds")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = logs.EnsureIndexKey("event")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = logs.EnsureIndex(mgo.Index{Key: []string{"channel", "open"}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = logs.EnsureIndex(mgo.Index{
|
||||
Key: []string{"shortId"},
|
||||
Unique: true,
|
||||
DropDups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &logRepository{
|
||||
logs: logs,
|
||||
posts: posts,
|
||||
shortIdCounter: newCounter(db, "auto_increment", "Log"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) {
|
||||
log := new(models.Log)
|
||||
err := r.logs.Find(bson.M{"$or": []bson.M{{"_id": id}, {"shortId": id}}}).One(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) {
|
||||
query := bson.M{}
|
||||
if filter.Search != nil {
|
||||
searchQuery := bson.M{
|
||||
"$text": bson.M{"$search": *filter.Search},
|
||||
"logId": bson.M{"$ne": nil},
|
||||
}
|
||||
|
||||
logIds := make([]string, 0, 64)
|
||||
err := r.posts.Find(searchQuery).Distinct("logId", &logIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query["shortId"] = bson.M{"$in": logIds}
|
||||
}
|
||||
if filter.Open != nil {
|
||||
r.openMutex.Lock()
|
||||
defer r.openMutex.Unlock()
|
||||
|
||||
query["open"] = filter.Open
|
||||
}
|
||||
if len(filter.Characters) > 0 {
|
||||
query["characterIds"] = bson.M{"$all": filter.Characters}
|
||||
}
|
||||
if len(filter.Channels) > 0 {
|
||||
query["channel"] = bson.M{"$in": filter.Channels}
|
||||
}
|
||||
if len(filter.Events) > 0 {
|
||||
query["event"] = bson.M{"$in": filter.Events}
|
||||
}
|
||||
if filter.MinDate != nil && filter.MaxDate != nil {
|
||||
query["date"] = bson.M{"$gte": *filter.MinDate, "$lte": *filter.MaxDate}
|
||||
} else if filter.MinDate != nil {
|
||||
query["date"] = bson.M{"$gte": *filter.MinDate}
|
||||
} else if filter.MaxDate != nil {
|
||||
query["date"] = bson.M{"$lt": *filter.MaxDate}
|
||||
}
|
||||
|
||||
logs := make([]*models.Log, 0, 32)
|
||||
err := r.logs.Find(query).Sort("-date").Limit(filter.Limit).All(&logs)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) Insert(ctx context.Context, log models.Log) (*models.Log, error) {
|
||||
nextShortId, err := r.shortIdCounter.Increment(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.ID = generate.LogID(log)
|
||||
log.ShortID = "L" + strconv.Itoa(nextShortId)
|
||||
|
||||
if log.Open {
|
||||
// There can be only one open log in the same channel.
|
||||
r.openMutex.Lock()
|
||||
defer r.openMutex.Unlock()
|
||||
|
||||
_, err = r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
|
||||
if err != nil {
|
||||
return nil, errors.New("Cannot close other logs: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err = r.logs.Insert(&log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &log, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) Update(ctx context.Context, log models.Log, update models.LogUpdate) (*models.Log, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Open != nil {
|
||||
if *update.Open == true {
|
||||
// There can be only one open log in the same channel.
|
||||
r.openMutex.Lock()
|
||||
defer r.openMutex.Unlock()
|
||||
|
||||
_, err := r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
|
||||
if err != nil {
|
||||
return nil, errors.New("Cannot close other logs: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
updateBson["open"] = *update.Open
|
||||
log.Open = *update.Open
|
||||
}
|
||||
if update.Title != nil {
|
||||
updateBson["title"] = *update.Title
|
||||
log.Title = *update.Title
|
||||
}
|
||||
if update.Description != nil {
|
||||
updateBson["description"] = *update.Description
|
||||
log.Description = *update.Description
|
||||
}
|
||||
if update.EventName != nil {
|
||||
updateBson["event"] = *update.EventName
|
||||
log.EventName = *update.EventName
|
||||
}
|
||||
if update.CharacterIDs != nil {
|
||||
updateBson["characterIds"] = update.CharacterIDs
|
||||
log.CharacterIDs = update.CharacterIDs
|
||||
}
|
||||
|
||||
err := r.logs.UpdateId(log.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &log, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) Delete(ctx context.Context, log models.Log) error {
|
||||
err := r.logs.RemoveId(log.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = r.posts.RemoveAll(bson.M{"logId": log.ShortID})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type postRepository struct {
|
||||
logs *mgo.Collection
|
||||
posts *mgo.Collection
|
||||
|
||||
orderMutex sync.Mutex
|
||||
}
|
||||
|
||||
func newPostRepository(db *mgo.Database) (*postRepository, error) {
|
||||
posts := db.C("logbot3.posts")
|
||||
|
||||
err := posts.EnsureIndexKey("logId")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = posts.EnsureIndexKey("time")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = posts.EnsureIndexKey("kind")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = posts.EnsureIndexKey("position")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = posts.EnsureIndex(mgo.Index{
|
||||
Key: []string{"$text:text"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &postRepository{
|
||||
posts: posts,
|
||||
logs: db.C("logbot3.logs"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Find(ctx context.Context, id string) (*models.Post, error) {
|
||||
post := new(models.Post)
|
||||
err := r.posts.FindId(id).One(post)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) List(ctx context.Context, filter models.PostFilter) ([]*models.Post, error) {
|
||||
query := bson.M{}
|
||||
if filter.LogID != nil && *filter.LogID != "" {
|
||||
logId := *filter.LogID
|
||||
if !strings.HasPrefix(logId, "L") {
|
||||
// Resolve long id to short id
|
||||
log := new(models.Log)
|
||||
err := r.logs.FindId(logId).Select(bson.M{"logId": 1, "_id": 1}).One(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logId = log.ShortID
|
||||
}
|
||||
|
||||
query["logId"] = logId
|
||||
}
|
||||
if len(filter.IDs) > 0 {
|
||||
query["_id"] = bson.M{"$in": filter.IDs}
|
||||
}
|
||||
if len(filter.Kinds) > 0 {
|
||||
query["kind"] = bson.M{"$in": filter.Kinds}
|
||||
}
|
||||
if filter.Search != nil {
|
||||
query["$text"] = bson.M{"$search": *filter.Search}
|
||||
}
|
||||
|
||||
posts := make([]*models.Post, 0, 32)
|
||||
err := r.posts.Find(query).Sort("-logId", "position").Limit(filter.Limit).All(&posts)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return []*models.Post{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Insert(ctx context.Context, post models.Post) (*models.Post, error) {
|
||||
r.orderMutex.Lock()
|
||||
defer r.orderMutex.Unlock()
|
||||
|
||||
lastPost := new(models.Post)
|
||||
err := r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
post.ID = generate.PostID()
|
||||
post.Position = lastPost.Position + 1 // Position 1 is first position, so this is safe.
|
||||
|
||||
err = r.posts.Insert(post)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) InsertMany(ctx context.Context, posts ...*models.Post) ([]*models.Post, error) {
|
||||
if len(posts) == 0 {
|
||||
return []*models.Post{}, nil
|
||||
}
|
||||
|
||||
logId := posts[0].LogID
|
||||
for _, post := range posts {
|
||||
if post.LogID != logId {
|
||||
return nil, repositories.ErrParentMismatch
|
||||
}
|
||||
|
||||
post.ID = generate.PostID()
|
||||
}
|
||||
|
||||
r.orderMutex.Lock()
|
||||
defer r.orderMutex.Unlock()
|
||||
|
||||
lastPost := new(models.Post)
|
||||
err := r.posts.Find(bson.M{"logId": posts[0].LogID}).Sort("-position").One(lastPost)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docs := make([]interface{}, len(posts))
|
||||
for i := range posts {
|
||||
posts[i].Position = lastPost.Position + 1 + i
|
||||
docs[i] = posts[i]
|
||||
}
|
||||
|
||||
err = r.posts.Insert(docs...)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Update(ctx context.Context, post models.Post, update models.PostUpdate) (*models.Post, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Time != nil {
|
||||
updateBson["time"] = *update.Time
|
||||
post.Time = *update.Time
|
||||
}
|
||||
if update.Kind != nil {
|
||||
updateBson["kind"] = *update.Kind
|
||||
post.Kind = *update.Kind
|
||||
}
|
||||
if update.Nick != nil {
|
||||
updateBson["nick"] = *update.Nick
|
||||
post.Nick = *update.Nick
|
||||
}
|
||||
if update.Text != nil {
|
||||
updateBson["text"] = *update.Text
|
||||
post.Text = *update.Text
|
||||
}
|
||||
|
||||
err := r.posts.UpdateId(post.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Move(ctx context.Context, post models.Post, position int) ([]*models.Post, error) {
|
||||
// If only MongoDB transactions weren't awful, this function would have been safe.
|
||||
// Since it isn't, then good luck.
|
||||
|
||||
// Validate lower position bound.
|
||||
if position < 1 {
|
||||
return nil, repositories.ErrInvalidPosition
|
||||
}
|
||||
|
||||
// Determine the operations on adjacent posts
|
||||
var resultFilter bson.M
|
||||
var pushFilter bson.M
|
||||
var increment int
|
||||
if post.Position > position {
|
||||
pushFilter = bson.M{"position": bson.M{
|
||||
"$gte": position,
|
||||
"$lt": post.Position,
|
||||
}}
|
||||
increment = 1
|
||||
resultFilter = bson.M{"position": bson.M{
|
||||
"$lte": post.Position,
|
||||
"$gte": position,
|
||||
}}
|
||||
} else {
|
||||
pushFilter = bson.M{"position": bson.M{
|
||||
"$lte": position,
|
||||
"$gt": post.Position,
|
||||
}}
|
||||
increment = -1
|
||||
resultFilter = bson.M{"position": bson.M{
|
||||
"$lte": position,
|
||||
"$gte": post.Position,
|
||||
}}
|
||||
}
|
||||
pushFilter["logId"] = post.LogID
|
||||
resultFilter["logId"] = post.LogID
|
||||
|
||||
// From here on out, sync is required
|
||||
r.orderMutex.Lock()
|
||||
defer r.orderMutex.Unlock()
|
||||
|
||||
// Detect ninja shenanigans
|
||||
post2 := new(models.Post)
|
||||
err := r.posts.FindId(post.ID).One(post2)
|
||||
if err != nil && post2.Position != post.Position {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
// Validate upper position bound
|
||||
lastPost := new(models.Post)
|
||||
err = r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
if position > lastPost.Position {
|
||||
return nil, repositories.ErrInvalidPosition
|
||||
}
|
||||
|
||||
// Move the posts
|
||||
changeInfo, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": increment}})
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
err = r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": position}})
|
||||
if err != nil {
|
||||
// Try to undo it
|
||||
_, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": -increment}})
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]*models.Post, 0, changeInfo.Matched+1)
|
||||
err = r.posts.Find(resultFilter).All(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Delete(ctx context.Context, post models.Post) error {
|
||||
r.orderMutex.Lock()
|
||||
defer r.orderMutex.Unlock()
|
||||
|
||||
err := r.posts.RemoveId(post.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = r.posts.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *postRepository) fixPositions(logRepo *logRepository) {
|
||||
disorders := make([]int, 0, 16)
|
||||
diffs := make([]int, 0, 16)
|
||||
startTime := time.Now()
|
||||
|
||||
timeout, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
logs, err := logRepo.List(timeout, models.LogFilter{})
|
||||
if err != nil {
|
||||
log.Println("Failed to get logs for position fix:", err)
|
||||
}
|
||||
|
||||
log.Println("Starting log position fixing, this should not take longer than 10 seconds.")
|
||||
|
||||
for _, l := range logs {
|
||||
r.orderMutex.Lock()
|
||||
|
||||
posts, err := r.List(timeout, models.PostFilter{LogID: &l.ShortID})
|
||||
if err != nil {
|
||||
r.orderMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
disorders = disorders[:0]
|
||||
diffs = diffs[:0]
|
||||
for i, post := range posts {
|
||||
if post.Position != (i + 1) {
|
||||
disorders = append(disorders, i)
|
||||
diffs = append(diffs, post.Position-(i+1))
|
||||
}
|
||||
}
|
||||
|
||||
if len(disorders) > 0 {
|
||||
log.Println(len(disorders), "order errors detected in", l.ID)
|
||||
|
||||
ops := 0
|
||||
|
||||
for i, post := range posts {
|
||||
if (i + 1) != posts[i].Position {
|
||||
ops++
|
||||
|
||||
err := r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": i + 1}})
|
||||
if err != nil {
|
||||
log.Println(l.ShortID, "fix failed after", ops, "ops:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Println(l.ShortID, "fixed after", ops, "ops.")
|
||||
}
|
||||
|
||||
r.orderMutex.Unlock()
|
||||
}
|
||||
|
||||
log.Println("Log position fixing finished in", time.Since(startTime))
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"log"
|
||||
)
|
||||
|
||||
type storyRepository struct {
|
||||
stories *mgo.Collection
|
||||
chapters *mgo.Collection
|
||||
comments *mgo.Collection
|
||||
}
|
||||
|
||||
func newStoryRepository(db *mgo.Database) (repositories.StoryRepository, error) {
|
||||
collection := db.C("story.stories")
|
||||
|
||||
err := collection.EnsureIndexKey("tags")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("updatedDate")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("fictionalDate")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("listed")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &storyRepository{
|
||||
stories: collection,
|
||||
chapters: db.C("story.chapters"),
|
||||
comments: db.C("story.comments"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *storyRepository) Find(ctx context.Context, id string) (*models.Story, error) {
|
||||
story := new(models.Story)
|
||||
err := r.stories.FindId(id).One(story)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return story, nil
|
||||
}
|
||||
|
||||
func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) {
|
||||
query := bson.M{}
|
||||
if filter.Author != nil {
|
||||
query["author"] = *filter.Author
|
||||
}
|
||||
if filter.Category != nil {
|
||||
query["category"] = *filter.Category
|
||||
}
|
||||
if filter.Open != nil {
|
||||
query["open"] = *filter.Open
|
||||
}
|
||||
if len(filter.Tags) > 0 {
|
||||
query["tags"] = bson.M{"$all": filter.Tags}
|
||||
}
|
||||
if filter.Unlisted != nil {
|
||||
query["listed"] = !*filter.Unlisted
|
||||
} else {
|
||||
query["listed"] = true
|
||||
}
|
||||
if !filter.EarliestFictionalDate.IsZero() && !filter.LatestFictionalDate.IsZero() {
|
||||
query["fictionalDate"] = bson.M{
|
||||
"$gte": filter.EarliestFictionalDate,
|
||||
"$lte": filter.LatestFictionalDate,
|
||||
}
|
||||
} else if !filter.EarliestFictionalDate.IsZero() {
|
||||
query["fictionalDate"] = bson.M{
|
||||
"$gte": filter.EarliestFictionalDate,
|
||||
}
|
||||
} else if !filter.LatestFictionalDate.IsZero() {
|
||||
query["fictionalDate"] = bson.M{
|
||||
"$lte": filter.LatestFictionalDate,
|
||||
}
|
||||
}
|
||||
|
||||
stories := make([]*models.Story, 0, 32)
|
||||
err := r.stories.Find(query).Sort("-updatedDate").Limit(filter.Limit).All(&stories)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return stories, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stories, nil
|
||||
}
|
||||
|
||||
func (r *storyRepository) Insert(ctx context.Context, story models.Story) (*models.Story, error) {
|
||||
story.ID = generate.StoryID()
|
||||
|
||||
err := r.stories.Insert(story)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &story, nil
|
||||
}
|
||||
|
||||
func (r *storyRepository) Update(ctx context.Context, story models.Story, update models.StoryUpdate) (*models.Story, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Name != nil {
|
||||
updateBson["name"] = *update.Name
|
||||
story.Name = *update.Name
|
||||
}
|
||||
if update.Open != nil {
|
||||
updateBson["open"] = *update.Open
|
||||
story.Open = *update.Open
|
||||
}
|
||||
if update.Category != nil {
|
||||
updateBson["category"] = *update.Category
|
||||
story.Category = *update.Category
|
||||
}
|
||||
if update.Author != nil {
|
||||
updateBson["author"] = *update.Author
|
||||
story.Author = *update.Author
|
||||
}
|
||||
if update.FictionalDate != nil {
|
||||
updateBson["fictionalDate"] = *update.FictionalDate
|
||||
story.FictionalDate = *update.FictionalDate
|
||||
}
|
||||
if update.UpdatedDate != nil {
|
||||
updateBson["updatedDate"] = *update.UpdatedDate
|
||||
story.UpdatedDate = *update.UpdatedDate
|
||||
}
|
||||
if update.Listed != nil {
|
||||
updateBson["listed"] = *update.Listed
|
||||
story.Listed = *update.Listed
|
||||
}
|
||||
|
||||
err := r.stories.UpdateId(story.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &story, nil
|
||||
}
|
||||
|
||||
func (r *storyRepository) AddTag(ctx context.Context, story models.Story, tag models.Tag) error {
|
||||
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$addToSet": bson.M{"tags": tag}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ci.Updated == 0 {
|
||||
return repositories.ErrTagExists
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *storyRepository) RemoveTag(ctx context.Context, story models.Story, tag models.Tag) error {
|
||||
ci, err := r.stories.UpdateAll(bson.M{"_id": story.ID}, bson.M{"$pull": bson.M{"tags": tag}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ci.Updated == 0 {
|
||||
return repositories.ErrTagDoesNotExist
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *storyRepository) Delete(ctx context.Context, story models.Story) error {
|
||||
err := r.stories.RemoveId(story.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chapterIds := make([]string, 0, 8)
|
||||
err = r.chapters.Find(bson.M{"storyId": story.ID}).Distinct("_id", &chapterIds)
|
||||
if err != nil {
|
||||
log.Println("Failed to find chapterIds:", err)
|
||||
return nil
|
||||
}
|
||||
if len(chapterIds) > 0 {
|
||||
c1, err := r.chapters.RemoveAll(bson.M{"storyId": story.ID})
|
||||
if err != nil {
|
||||
log.Println("Failed to remove chapters:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
c2, err := r.comments.RemoveAll(bson.M{"chapterId": bson.M{"$in": chapterIds}})
|
||||
if err != nil {
|
||||
log.Println("Failed to remove comments:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Removed story %s (%d chapters, %d comments)", story.ID, c1.Removed, c2.Removed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type tagRepository struct {
|
||||
stories *mgo.Collection
|
||||
}
|
||||
|
||||
func newTagRepository(db *mgo.Database) repositories.TagRepository {
|
||||
return &tagRepository{
|
||||
stories: db.C("story.stories"),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tagRepository) Find(ctx context.Context, kind models.TagKind, name string) (*models.Tag, error) {
|
||||
tags := make([]*models.Tag, 0, 1)
|
||||
err := r.stories.Find(bson.M{"listed": true, "tags": bson.M{"kind": kind, "name": name}}).Distinct("tag", &tags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(tags) == 0 {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.Kind == kind && tag.Name == name {
|
||||
return tag, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
func (r *tagRepository) List(ctx context.Context, filter models.TagFilter) ([]*models.Tag, error) {
|
||||
tags := make([]*models.Tag, 0, 64)
|
||||
|
||||
query := bson.M{"listed": true, "tags": bson.M{"$ne": nil}}
|
||||
if filter.Kind != nil {
|
||||
query["tags.kind"] = *filter.Kind
|
||||
}
|
||||
|
||||
err := r.stories.Find(query).Distinct("tags", &tags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filter.Kind != nil {
|
||||
// While it's unsorted, delete any incorrect tags with replacement.
|
||||
for i := 0; i < len(tags); i++ {
|
||||
if tags[i].Kind != *filter.Kind {
|
||||
tags[i] = tags[len(tags)-1]
|
||||
tags = tags[:len(tags)-1]
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
if filter.Kind == nil {
|
||||
kindCmp := strings.Compare(string(tags[i].Kind), string(tags[j].Kind))
|
||||
if kindCmp != 0 {
|
||||
return kindCmp < 0
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Compare(tags[i].Name, tags[j].Name) < 0
|
||||
})
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
type userRepository struct {
|
||||
users *mgo.Collection
|
||||
}
|
||||
|
||||
func newUserRepository(db *mgo.Database) (repositories.UserRepository, error) {
|
||||
collection := db.C("core.users")
|
||||
|
||||
return &userRepository{users: collection}, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Find(ctx context.Context, id string) (*models.User, error) {
|
||||
user := new(models.User)
|
||||
err := r.users.FindId(id).One(user)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Insert(ctx context.Context, user models.User) (*models.User, error) {
|
||||
err := r.users.Insert(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
module git.aiterp.net/rpdata/api
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.9.0
|
||||
github.com/beorn7/perks v1.0.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e
|
||||
github.com/go-ini/ini v1.35.0 // indirect
|
||||
github.com/golang/protobuf v1.3.1 // indirect
|
||||
github.com/google/go-cmp v0.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.0 // indirect
|
||||
github.com/h2non/filetype v1.0.8
|
||||
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05
|
||||
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 // indirect
|
||||
github.com/prometheus/client_golang v0.9.1
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
|
||||
github.com/prometheus/common v0.4.0 // indirect
|
||||
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/tidwall/pretty v1.0.0 // indirect
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser v1.1.2
|
||||
go.mongodb.org/mongo-driver v1.0.3
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect
|
||||
golang.org/x/net v0.0.0-20190514140710-3ec191127204 // indirect
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
gopkg.in/ini.v1 v1.42.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
||||
|
||||
replace golang.org/x/net => golang.org/x/net v0.0.0-20180416171110-a35a21de978d
|
||||
@@ -0,0 +1,142 @@
|
||||
github.com/99designs/gqlgen v0.9.0 h1:g1arBPML74Vqv0L3Q+TqIhGXLspV+2MYtRLkBxuZrlE=
|
||||
github.com/99designs/gqlgen v0.9.0/go.mod h1:HrrG7ic9EgLPsULxsZh/Ti+p0HNWgR3XRuvnD0pb5KY=
|
||||
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e h1:hmrbHva/wKD6X4M7pgHVbg/KfVV1wwgr1NEkQaVD3rU=
|
||||
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-ini/ini v1.35.0 h1:D/my3+xOfqZMkJpciRcyqU7XMBUgiZa9qXjZIa8uv2k=
|
||||
github.com/go-ini/ini v1.35.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/h2non/filetype v1.0.8 h1:le8gpf+FQA0/DlDABbtisA1KiTS0Xi+YSC/E8yY3Y14=
|
||||
github.com/h2non/filetype v1.0.8/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05 h1:YEyFCqmHcmbcTLqZ7yEAg7VIUat6PuDzGbIctZy888k=
|
||||
github.com/minio/minio-go v0.0.0-20180409193742-3d2d02921f05/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
|
||||
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk=
|
||||
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e h1:zK8d1aZ+gw/Ne4uMfZTFRxj08PUOp+gGwm4HWUeGI1k=
|
||||
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
|
||||
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
|
||||
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||
go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU=
|
||||
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180416171110-a35a21de978d h1:O2P57H5Cc+d+DJos+iweraI9rmzMYwV+45vkZdDY0Oo=
|
||||
golang.org/x/net v0.0.0-20180416171110-a35a21de978d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5 h1:mzjBh+S5frKOsOBobWIMAbXavqjmgO17k/2puhcFR94=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6 h1:iZgcI2DDp6zW5v9Z/5+f0NuqoxNdmzg4hivjk2WLXpY=
|
||||
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
|
||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
||||
@@ -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,84 @@
|
||||
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
|
||||
}
|
||||
|
||||
return r.s.Logs.Import(ctx, input.Importer, date, tz, input.ChannelName, 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,81 @@
|
||||
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
|
||||
}
|
||||
|
||||
return r.s.Stories.CreateStory(ctx, input.Name, input.Author, input.Category, listed, open, tags, time.Now(), fictionalDate)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Filters for the channels query
|
||||
input ChannelsFilter {
|
||||
# Filter to either logged or unlogged channels
|
||||
logged: Boolean
|
||||
|
||||
# Filter by event name
|
||||
eventName: String
|
||||
|
||||
# Filter by location name
|
||||
locationName: String
|
||||
}
|
||||
|
||||
input ChannelAddInput {
|
||||
# The channel's name
|
||||
name: String!
|
||||
@@ -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!
|
||||
}
|
||||
|
||||
# Filter for characters query
|
||||
input CharactersFilter {
|
||||
# Filter by character IDs
|
||||
ids: [String!]
|
||||
|
||||
# Filter by nicks
|
||||
nicks: [String!]
|
||||
|
||||
# Filter by names
|
||||
names: [String!]
|
||||
|
||||
# Filter by author
|
||||
author: String
|
||||
|
||||
# Filter by text search matching against the character's description
|
||||
search: String
|
||||
}
|
||||
|
||||
# Input for adding characters
|
||||
input CharacterAddInput {
|
||||
# The primary IRC nick name to recognize this character by
|
||||
@@ -50,6 +68,7 @@ input CharacterNickInput {
|
||||
nick: String!
|
||||
}
|
||||
|
||||
# Inpuit for editCharacter mutation
|
||||
input CharacterEditInput {
|
||||
# The id for the character to edit
|
||||
id: String!
|
||||
@@ -62,4 +81,11 @@ input CharacterEditInput {
|
||||
|
||||
# A short description for the character
|
||||
description: String
|
||||
}
|
||||
}
|
||||
|
||||
# Input for removeCharacter mutation
|
||||
input CharacterRemoveInput {
|
||||
# The id for the character to edit
|
||||
id: String!
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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!
|
||||
|
||||
# The time of uploading
|
||||
time: String!
|
||||
time: Time!
|
||||
|
||||
# Whether the file is publicly listable. Someone with knowledge of the ID
|
||||
# will still be able to view it, however.
|
||||
@@ -29,9 +29,30 @@ type File {
|
||||
url: String
|
||||
}
|
||||
|
||||
# Filter for the files quiery.
|
||||
input FilesFilter {
|
||||
# If set, this will limit the results to either public or private files.
|
||||
public: Boolean
|
||||
|
||||
# Limit the MIME types of the files.
|
||||
mimeTypes: [String!]
|
||||
}
|
||||
|
||||
# Input for 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 EditFileInput {
|
||||
# The file's unique ID
|
||||
input FileEditInput {
|
||||
# The file's unique ID
|
||||
id: String!
|
||||
|
||||
# Whether the file is publicly listable. Someone with knowledge of the ID
|
||||
@@ -40,4 +61,10 @@ input EditFileInput {
|
||||
|
||||
# The file's name
|
||||
name: String
|
||||
}
|
||||
|
||||
# Input for removeFile mutation
|
||||
input FileRemoveInput {
|
||||
# The file's unique ID
|
||||
id: String!
|
||||
}
|
||||
@@ -8,7 +8,7 @@ type Log {
|
||||
shortId: String!
|
||||
|
||||
# The date for a log.
|
||||
date: String!
|
||||
date: Time!
|
||||
|
||||
# The channel of a log.
|
||||
channelName: String!
|
||||
@@ -21,7 +21,7 @@ type Log {
|
||||
|
||||
# The log's event, which is the same as the tags in the previous logbot site.
|
||||
# Empty string means that it's no event.
|
||||
event: String!
|
||||
eventName: String!
|
||||
|
||||
# The description of a session, which is empty if unset.
|
||||
description: String!
|
||||
@@ -34,10 +34,24 @@ type Log {
|
||||
|
||||
# The posts of the logfile, which can be filtered by kinds.
|
||||
posts(kinds:[String!]): [Post!]!
|
||||
|
||||
# Suggested next logs.
|
||||
nextLogs: [LogSuggestion!]!
|
||||
|
||||
# Suggested previous logs.
|
||||
prevLogs: [LogSuggestion!]!
|
||||
}
|
||||
|
||||
# Input for logs query
|
||||
input LogQueryInput {
|
||||
# Suggested log
|
||||
type LogSuggestion {
|
||||
log: Log!
|
||||
hasEvent: Boolean!
|
||||
hasChannel: Boolean!
|
||||
characters: [Character!]!
|
||||
}
|
||||
|
||||
# Filter for logs query
|
||||
input LogsFilter {
|
||||
# Channels to limit results to (inclusive)
|
||||
channels: [String!]
|
||||
|
||||
@@ -50,17 +64,17 @@ input LogQueryInput {
|
||||
# Search post content
|
||||
search: String
|
||||
|
||||
# Limit to only open logs
|
||||
# Limit by whether it's open or not
|
||||
open: Boolean
|
||||
|
||||
# Limit the amount of posts
|
||||
# Limit the amount of results you get back
|
||||
limit: Int
|
||||
}
|
||||
|
||||
# Input for addLog mutation
|
||||
input LogAddInput {
|
||||
# The date of the log, in RFC3339 format with up to nanosecond precision
|
||||
date: String!
|
||||
# The date of the log.
|
||||
date: Time!
|
||||
|
||||
# The channel where the log is set
|
||||
channel: String!
|
||||
@@ -96,37 +110,65 @@ input LogEditInput {
|
||||
open: Boolean
|
||||
}
|
||||
|
||||
# A LogHeader is a cut-down version of Log that does not allow getting the actual posts.
|
||||
type LogHeader {
|
||||
# A unique identifier for the log.
|
||||
# Input for removeLog mutation
|
||||
input LogRemoveInput {
|
||||
# The id of the log to remove
|
||||
id: String!
|
||||
}
|
||||
|
||||
# A secondary unique identifier for the log. This is a lot shorter and more suitable for storage when
|
||||
# the link doesn't need to be as expressive.
|
||||
shortId: String!
|
||||
|
||||
# The date for a log.
|
||||
date: String!
|
||||
|
||||
# The channel name of a log.
|
||||
"""
|
||||
Input for importLog mutation.
|
||||
"""
|
||||
input LogImportInput {
|
||||
"The channel of the log, alwyas required."
|
||||
channelName: String!
|
||||
|
||||
# Information about the channel this log is in.
|
||||
channel: Channel!
|
||||
|
||||
# The session's title.
|
||||
title: String!
|
||||
"Which importer to use."
|
||||
importer: LogImporter!
|
||||
|
||||
# The log's event, which is the same as the tags in the previous logbot site.
|
||||
# Empty string means that it's no event.
|
||||
event: String!
|
||||
"The timezone of the import, all log and post dates will be treated as it. It will be UTC if none is specified."
|
||||
timezone: String
|
||||
|
||||
# The description of a session, which is empty if unset.
|
||||
description: String!
|
||||
"The date of the log, if not provided in the log body."
|
||||
date: Time
|
||||
|
||||
# Whether the log session is open.
|
||||
open: Boolean!
|
||||
|
||||
# The characters involved in the log file.
|
||||
characters: [Character!]!
|
||||
"The log body itself."
|
||||
data: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Input for splitLog mutation.
|
||||
"""
|
||||
input LogSplitInput {
|
||||
"Log ID"
|
||||
logId: String!
|
||||
|
||||
"Split out all posts from and including the post with this ID"
|
||||
startPostId: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Input for mergeLog mutation.
|
||||
"""
|
||||
input LogMergeInput {
|
||||
"Target log ID"
|
||||
targetLogId: String!
|
||||
|
||||
"Source log ID"
|
||||
sourceLogId: String!
|
||||
|
||||
"Remove the source log after merging"
|
||||
removeAfter: Boolean
|
||||
}
|
||||
|
||||
enum LogImporter {
|
||||
"""
|
||||
mIRC-like: This importer parses logs that copied from mIRC posts without spaces.
|
||||
"""
|
||||
MircLike
|
||||
|
||||
"""
|
||||
Forum log: This importer parses the format on the forum. The displayed log, not the post source.
|
||||
"""
|
||||
ForumLog
|
||||
}
|
||||
@@ -7,7 +7,7 @@ type Post {
|
||||
logId: String!
|
||||
|
||||
# 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'.
|
||||
kind: String!
|
||||
@@ -23,12 +23,12 @@ type Post {
|
||||
}
|
||||
|
||||
# Input for the addPost mutation
|
||||
input AddPostInput {
|
||||
input PostAddInput {
|
||||
# The log's ID that this post should be a part of
|
||||
logId: String!
|
||||
|
||||
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
||||
time: String!
|
||||
time: Time!
|
||||
|
||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||
kind: String!
|
||||
@@ -41,12 +41,12 @@ input AddPostInput {
|
||||
}
|
||||
|
||||
# Input for the editPost mutation
|
||||
input EditPostInput {
|
||||
input PostEditInput {
|
||||
# The Post ID
|
||||
id: String!
|
||||
|
||||
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
||||
time: String
|
||||
time: Time
|
||||
|
||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||
kind: String
|
||||
@@ -59,10 +59,24 @@ input EditPostInput {
|
||||
}
|
||||
|
||||
# Input for the movePost mutation
|
||||
input MovePostInput {
|
||||
input PostMoveInput {
|
||||
# The Post ID
|
||||
id: String!
|
||||
|
||||
# Target index
|
||||
toPosition: Int!
|
||||
}
|
||||
|
||||
# Input for the removePost mutation
|
||||
input PostRemoveInput {
|
||||
# The Post ID
|
||||
id: String!
|
||||
}
|
||||
|
||||
# Filter for posts query
|
||||
input PostsFilter {
|
||||
ids: [String!]
|
||||
kinds: [String!]
|
||||
logId: String
|
||||
limit: Int
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
# A Story is a piece of content that, unlike wiki articles, are not something that should be ordered by time instead of title. It can
|
||||
# contain multiple chapters by one or more autohrs
|
||||
type Story {
|
||||
# The story's ID
|
||||
id: String!
|
||||
|
||||
# The story's name, which will show up in the lists.
|
||||
name: String!
|
||||
|
||||
# The story's author
|
||||
author: String!
|
||||
|
||||
# Whether other users may add/edit chapters to the story.
|
||||
open: Boolean!
|
||||
|
||||
# Whether users without a direct link can find this story.
|
||||
listed: Boolean!
|
||||
|
||||
# The category of the story
|
||||
category: StoryCategory!
|
||||
|
||||
# The tags for this tory
|
||||
tags: [Tag!]!
|
||||
|
||||
# The chapters of this story
|
||||
chapters: [Chapter!]!
|
||||
|
||||
# The date the story was created.
|
||||
createdDate: 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!
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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!
|
||||
}
|
||||
|
||||
# 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
|
||||
enum TagKind {
|
||||
# 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.
|
||||
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}
|
||||
}
|
||||
+53
-25
@@ -5,55 +5,83 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var globalMutex sync.Mutex
|
||||
var global *Config
|
||||
|
||||
// Config is configuration
|
||||
// Config is configuration data
|
||||
type Config struct {
|
||||
Space struct {
|
||||
Host string `json:"host"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Bucket string `json:"bucket"`
|
||||
MaxSize int64 `json:"maxSize"`
|
||||
Root string `json:"root"`
|
||||
} `json:"space"`
|
||||
Space Space `json:"space" yaml:"space"`
|
||||
Database Database `json:"database" yaml:"database"`
|
||||
Wiki Wiki `json:"wiki" yaml:"wiki"`
|
||||
}
|
||||
|
||||
Database struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Db string `json:"db"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Mechanism string `json:"mechanism"`
|
||||
} `json:"database"`
|
||||
// Space is configuration for spaces.
|
||||
type Space struct {
|
||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
AccessKey string `json:"accessKey" yaml:"accessKey"`
|
||||
SecretKey string `json:"secretKey" yaml:"secretKey"`
|
||||
Bucket string `json:"bucket" yaml:"bucket"`
|
||||
MaxSize int64 `json:"maxSize" yaml:"maxSize"`
|
||||
Root string `json:"root" yaml:"root"`
|
||||
URLRoot string `json:"urlRoot" yaml:"urlRoot"`
|
||||
}
|
||||
|
||||
Wiki struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"wiki"`
|
||||
// Database is configuration for spaces.
|
||||
type Database struct {
|
||||
Driver string `json:"driver" yaml:"driver"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
Port int `json:"port" yaml:"port"`
|
||||
Db string `json:"db" yaml:"db"`
|
||||
Username string `json:"username" yaml:"username"`
|
||||
Password string `json:"password" yaml:"password"`
|
||||
Mechanism string `json:"mechanism" yaml:"mechanism"`
|
||||
}
|
||||
|
||||
// Wiki is the wiki stuff.
|
||||
type Wiki struct {
|
||||
URL string `json:"url" yaml:"url"`
|
||||
}
|
||||
|
||||
// Load loads config stuff
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
func (config *Config) LoadAny(filenames ...string) error {
|
||||
for _, filename := range filenames {
|
||||
if err := config.Load(filename); err == nil {
|
||||
return nil
|
||||
if err := config.Load(filename); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
*config = Config{}
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("Failed to load configuration files")
|
||||
@@ -64,7 +92,7 @@ func Global() Config {
|
||||
globalMutex.Lock()
|
||||
if global == nil {
|
||||
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 {
|
||||
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()))
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Change represents a change in the rpdata history through the API.
|
||||
type Change struct {
|
||||
ID string `bson:"_id"`
|
||||
Model ChangeModel `bson:"model"`
|
||||
Op string `bson:"op"`
|
||||
Author string `bson:"author"`
|
||||
Listed bool `bson:"listed"`
|
||||
Keys []ChangeKey `bson:"keys"`
|
||||
Date time.Time `bson:"date"`
|
||||
|
||||
Logs []*Log `bson:"logs"`
|
||||
Characters []*Character `bson:"characters"`
|
||||
Channels []*Channel `bson:"channels"`
|
||||
Posts []*Post `bson:"posts"`
|
||||
Stories []*Story `bson:"stories"`
|
||||
Tags []*Tag `bson:"tags"`
|
||||
Chapters []*Chapter `bson:"chapters"`
|
||||
Comments []*Comment `bson:"comments"`
|
||||
Files []*File `bson:"files"`
|
||||
}
|
||||
|
||||
// AddObject adds the model into the appropriate array.
|
||||
func (change *Change) AddObject(object interface{}) bool {
|
||||
if v := reflect.ValueOf(object); v.Kind() == reflect.Struct {
|
||||
ptr := reflect.PtrTo(v.Type())
|
||||
ptrValue := reflect.New(ptr.Elem())
|
||||
ptrValue.Elem().Set(v)
|
||||
|
||||
object = ptrValue.Interface()
|
||||
}
|
||||
|
||||
switch object := object.(type) {
|
||||
case *Log:
|
||||
change.Logs = append(change.Logs, object)
|
||||
case []*Log:
|
||||
change.Logs = append(change.Logs, object...)
|
||||
case *Character:
|
||||
change.Characters = append(change.Characters, object)
|
||||
case []*Character:
|
||||
change.Characters = append(change.Characters, object...)
|
||||
case *Channel:
|
||||
change.Channels = append(change.Channels, object)
|
||||
case []*Channel:
|
||||
change.Channels = append(change.Channels, object...)
|
||||
case *Post:
|
||||
change.Posts = append(change.Posts, object)
|
||||
case []*Post:
|
||||
change.Posts = append(change.Posts, object...)
|
||||
case *Story:
|
||||
change.Stories = append(change.Stories, object)
|
||||
case []*Story:
|
||||
change.Stories = append(change.Stories, object...)
|
||||
case *Tag:
|
||||
change.Tags = append(change.Tags, object)
|
||||
case []*Tag:
|
||||
change.Tags = append(change.Tags, object...)
|
||||
case *Chapter:
|
||||
change.Chapters = append(change.Chapters, object)
|
||||
case []*Chapter:
|
||||
change.Chapters = append(change.Chapters, object...)
|
||||
case *Comment:
|
||||
change.Comments = append(change.Comments, object)
|
||||
case []*Comment:
|
||||
change.Comments = append(change.Comments, object...)
|
||||
case *File:
|
||||
change.Files = append(change.Files, object)
|
||||
case []*File:
|
||||
change.Files = append(change.Files, object...)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Objects makes a combined, mixed array of all the models stored in this change.
|
||||
func (change *Change) Objects() []interface{} {
|
||||
data := make([]interface{}, 0, 8)
|
||||
|
||||
for _, log := range change.Logs {
|
||||
data = append(data, log)
|
||||
}
|
||||
for _, channel := range change.Channels {
|
||||
data = append(data, channel)
|
||||
}
|
||||
for _, character := range change.Characters {
|
||||
data = append(data, character)
|
||||
}
|
||||
for _, post := range change.Posts {
|
||||
data = append(data, post)
|
||||
}
|
||||
for _, story := range change.Stories {
|
||||
data = append(data, story)
|
||||
}
|
||||
for _, tag := range change.Tags {
|
||||
data = append(data, tag)
|
||||
}
|
||||
for _, chapter := range change.Chapters {
|
||||
data = append(data, chapter)
|
||||
}
|
||||
for _, comment := range change.Comments {
|
||||
data = append(data, comment)
|
||||
}
|
||||
for _, file := range change.Files {
|
||||
data = append(data, file)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (change *Change) PassesFilter(filter ChangeFilter) bool {
|
||||
if filter.Author != nil && change.Author != *filter.Author {
|
||||
return false
|
||||
}
|
||||
|
||||
// For unlisted changes, pass it only if the filter refers to the specific index.
|
||||
if !change.Listed {
|
||||
hasSpecificKey := false
|
||||
|
||||
KeyFindLoop:
|
||||
for _, key := range filter.Keys {
|
||||
if key.ID == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, changeKey := range change.Keys {
|
||||
if changeKey.Model == key.Model && changeKey.ID == key.ID {
|
||||
hasSpecificKey = true
|
||||
break KeyFindLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSpecificKey {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if filter.EarliestDate != nil && filter.EarliestDate.Before(change.Date) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(filter.Keys) > 0 {
|
||||
foundKey := false
|
||||
|
||||
KeyFindLoop2:
|
||||
for _, key := range filter.Keys {
|
||||
for _, changeKey := range change.Keys {
|
||||
if changeKey == key {
|
||||
foundKey = true
|
||||
break KeyFindLoop2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundKey {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ChangeKey is a key for a change that can be used when subscribing to them.
|
||||
type ChangeKey struct {
|
||||
Model ChangeModel `bson:"model"`
|
||||
ID string `bson:"id"`
|
||||
}
|
||||
|
||||
// ChangeFilter is a filter for listing changes.
|
||||
type ChangeFilter struct {
|
||||
Keys []ChangeKey
|
||||
EarliestDate *time.Time
|
||||
Author *string
|
||||
Limit *int
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package changekeys
|
||||
|
||||
import "git.aiterp.net/rpdata/api/models"
|
||||
|
||||
// All makes a changelog that's (model, "*"). It's a helper to standardize it.
|
||||
func All(model string) models.ChangeKey {
|
||||
return models.ChangeKey{Model: models.ChangeModel(model), ID: "*"}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user