Compare commits
31
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 |
+3
-1
@@ -8,6 +8,8 @@ rpdata-graphiql
|
||||
rpdata-*
|
||||
!rpdata-*/
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
/graph2/graphcore/*_gen.go
|
||||
generated.gql
|
||||
generated.go
|
||||
generated.go
|
||||
|
||||
@@ -12,11 +12,6 @@ RUN go generate ./...
|
||||
# Build server and tools (CGO disabled to make them 100% static)
|
||||
ENV CGO_ENABLED 0
|
||||
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-server ./cmd/rpdata-server
|
||||
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2charimport ./cmd/rpdata-lb2charimport
|
||||
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-lb2logimport ./cmd/rpdata-lb2logimport
|
||||
# TODO: RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-wikifileimport ./cmd/rpdata-wikifileimport
|
||||
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-ensurechannels ./cmd/rpdata-ensurechannels
|
||||
RUN go build -installsuffix cgo -ldflags="-s -w" -o /binaries/rpdata-as2storyimport ./cmd/rpdata-as2storyimport
|
||||
|
||||
## 2. Distribute
|
||||
# Use alpine linux
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
"git.aiterp.net/rpdata/api/models/stories"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var flagHost = flag.String("host", "127.0.0.1:3306", "SQL host")
|
||||
var flagDB = flag.String("database", "aitestory", "SQL database")
|
||||
var flagUser = flag.String("user", "aitestory", "SQL user")
|
||||
var flagPassword = flag.String("password", "", "SQL password")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
db, err := sqlx.Connect("mysql", fmt.Sprintf("%s:%s@(%s)/%s", *flagUser, *flagPassword, *flagHost, *flagDB))
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
store.Init()
|
||||
|
||||
results := make([]storyResult, 0, 64)
|
||||
rows, err := db.Queryx("SELECT * FROM page WHERE unlisted=0;")
|
||||
for rows.Next() {
|
||||
result := storyResult{}
|
||||
err := rows.StructScan(&result)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
tagResults := make([]tagResult, 0, 256)
|
||||
rows, err = db.Queryx("SELECT page_id,type,name FROM page_tag LEFT JOIN tag ON tag_id=tag.id;")
|
||||
for rows.Next() {
|
||||
result := tagResult{}
|
||||
err := rows.StructScan(&result)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
tagResults = append(tagResults, result)
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
fictionalDate, err := time.Parse("2006-01-02 15:04:05", result.FictionalDate)
|
||||
if err != nil {
|
||||
if result.FictionalDate != "0000-00-00 00:00:00" {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
if fictionalDate.Year() < 1800 {
|
||||
fictionalDate = time.Time{}
|
||||
}
|
||||
|
||||
publishDate, err := time.Parse("2006-01-02 15:04:05", result.PublishDate)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
tags := make([]models.Tag, 0, 8)
|
||||
for _, tagResult := range tagResults {
|
||||
if tagResult.PageID == result.ID {
|
||||
tags = append(tags, models.Tag{Kind: models.TagKind(tagResult.Type), Name: tagResult.Name})
|
||||
}
|
||||
}
|
||||
|
||||
category := models.StoryCategory(result.Category)
|
||||
if !category.IsValid() {
|
||||
log.Println(result.Name, "does not have a valid category:", result.Category)
|
||||
continue
|
||||
}
|
||||
|
||||
story, err := stories.Add(result.Name, result.Author[5:], category, true, false, tags, publishDate, fictionalDate)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Change the story title
|
||||
title := result.Name
|
||||
if strings.HasPrefix(result.Source, "#") {
|
||||
firstNewline := strings.Index(result.Source, "\n")
|
||||
result.Name = strings.Replace(result.Source[1:firstNewline], "\r", "", -1)
|
||||
result.Source = result.Source[firstNewline+1:]
|
||||
|
||||
result.Source = strings.Replace(result.Source, "\r\n", "\n", -1)
|
||||
if strings.HasPrefix(result.Source, "\r") || strings.HasPrefix(result.Source, "\n") {
|
||||
result.Source = result.Source[1:]
|
||||
}
|
||||
}
|
||||
|
||||
chapter, err := chapters.Add(story, title, result.Author[5:], result.Source, publishDate, &fictionalDate, models.ChapterCommentModeDisabled)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
fmt.Println(result.ID, "->", story.ID, chapter.ID)
|
||||
}
|
||||
}
|
||||
|
||||
type tagResult struct {
|
||||
PageID string `db:"page_id"`
|
||||
Type string `db:"type"`
|
||||
Name string `db:"name"`
|
||||
}
|
||||
|
||||
type storyResult struct {
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
Author string `db:"author"`
|
||||
Category string `db:"category"`
|
||||
FictionalDate string `db:"fictional_date"`
|
||||
PublishDate string `db:"publish_date"`
|
||||
EditDate string `db:"edit_date"`
|
||||
Unlisted bool `db:"unlisted"`
|
||||
Dated bool `db:"dated"`
|
||||
Spesific bool `db:"specific"`
|
||||
Indexed bool `db:"indexed"`
|
||||
Published bool `db:"published"`
|
||||
Type string `db:"type"`
|
||||
Source string `db:"source"`
|
||||
Cache string `db:"cache"`
|
||||
BackgroundURL *string `db:"background_url"`
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/models/channels"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := store.Init()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
|
||||
logs, err := logs.List(nil)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
|
||||
added := make(map[string]bool, 1024)
|
||||
for _, log := range logs {
|
||||
if added[log.ChannelName] {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := channels.Ensure(log.ChannelName, false)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, log.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
added[log.ChannelName] = true
|
||||
|
||||
fmt.Println(log.ChannelName, "ensured")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
characters.json
|
||||
characters.cson
|
||||
debug
|
||||
@@ -1,55 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type charInfo struct {
|
||||
Nicks []string `json:"nicks"`
|
||||
Name string `json:"name"`
|
||||
Author string `json:"player"`
|
||||
ShortName string `json:"first"`
|
||||
}
|
||||
|
||||
func load(reader io.Reader) ([]charInfo, error) {
|
||||
data := make(map[string]interface{})
|
||||
err := json.NewDecoder(reader).Decode(&data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
links := make(map[string]string, len(data))
|
||||
infos := make([]charInfo, 0, 64)
|
||||
|
||||
for key, value := range data {
|
||||
if info, ok := value.(map[string]interface{}); ok {
|
||||
name := info["name"].(string)
|
||||
author := info["player"].(string)
|
||||
shortName, ok := info["first"].(string)
|
||||
if !ok {
|
||||
shortName = strings.SplitN(name, " ", 2)[0]
|
||||
}
|
||||
|
||||
infos = append(infos, charInfo{
|
||||
Nicks: []string{key},
|
||||
Name: name,
|
||||
Author: author,
|
||||
ShortName: shortName,
|
||||
})
|
||||
} else if nick, ok := value.(string); ok {
|
||||
links[key] = nick
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range links {
|
||||
for i := range infos {
|
||||
if infos[i].Nicks[0] == value {
|
||||
infos[i].Nicks = append(infos[i].Nicks, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return infos, nil
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/models/characters"
|
||||
)
|
||||
|
||||
var fileName = flag.String("file", "./characters.json", "json file to load")
|
||||
|
||||
func main() {
|
||||
file, err := os.Open(*fileName)
|
||||
if err != nil {
|
||||
log.Fatalln("Open:", err)
|
||||
}
|
||||
|
||||
infos, err := load(file)
|
||||
if err != nil {
|
||||
log.Fatalln("Parse:", err)
|
||||
}
|
||||
|
||||
err = store.Init()
|
||||
if err != nil {
|
||||
log.Fatalln("Store:", err)
|
||||
}
|
||||
|
||||
charsAdded, charsFailed := 0, 0
|
||||
nicksAdded, nicksFailed := 0, 0
|
||||
|
||||
for _, info := range infos {
|
||||
char, err := characters.Add(info.Nicks[0], info.Name, info.ShortName, info.Author, "")
|
||||
if err != nil {
|
||||
log.Println(info.Nicks[0], "failed to insert:", err)
|
||||
charsFailed++
|
||||
|
||||
char, err = characters.FindNick(info.Nicks[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
log.Println(info.Nicks[0], "added")
|
||||
charsAdded++
|
||||
}
|
||||
|
||||
for _, alt := range info.Nicks[1:] {
|
||||
char, err = characters.AddNick(char, alt)
|
||||
if err != nil {
|
||||
log.Println(info.Nicks[0], "failed to add nick", alt, "error:", err)
|
||||
nicksFailed++
|
||||
} else {
|
||||
log.Println(info.Nicks[0], "addded nick", alt)
|
||||
nicksAdded++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Characters – %d/%d", charsAdded, charsFailed+charsAdded)
|
||||
log.Printf("Alt. Nicks – %d/%d", nicksAdded, nicksFailed+nicksAdded)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
logs
|
||||
debug
|
||||
@@ -1,85 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type logLine struct {
|
||||
Verb string
|
||||
Args []string
|
||||
Text string
|
||||
}
|
||||
|
||||
func parseFile(reader io.Reader) []logLine {
|
||||
bufReader := bufio.NewReader(reader)
|
||||
results := make([]logLine, 0, 512)
|
||||
|
||||
for {
|
||||
line, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
if len(line) < 2 {
|
||||
break
|
||||
}
|
||||
} else if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if len(line) <= 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
line = strings.Replace(line, "\n", "", 1)
|
||||
line = strings.Replace(line, "\r", "", 1)
|
||||
|
||||
results = append(results, parseLine(line))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func parseLine(line string) logLine {
|
||||
textSplit := strings.SplitN(line, " :", 2)
|
||||
tokens := strings.Split(textSplit[0], " ")
|
||||
|
||||
ll := logLine{
|
||||
Verb: tokens[0],
|
||||
}
|
||||
|
||||
if len(tokens) > 1 {
|
||||
ll.Args = tokens[1:]
|
||||
}
|
||||
|
||||
if len(textSplit) > 1 {
|
||||
ll.Text = textSplit[1]
|
||||
}
|
||||
|
||||
return ll
|
||||
}
|
||||
|
||||
func parseFilename(fname string) (date time.Time, channel string, err error) {
|
||||
date, err = time.ParseInLocation("2006-01-02_150405", fname[:17], time.Local)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ms, err := strconv.Atoi(fname[17:20])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
date = date.Add(time.Duration(ms) * time.Millisecond)
|
||||
|
||||
if len(fname) < 23 {
|
||||
err = errors.New("filename too short")
|
||||
return
|
||||
}
|
||||
|
||||
channel = fname[21:]
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/posts"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
)
|
||||
|
||||
var prefixReplacer = strings.NewReplacer("+", "", "@", "", "!", "", "%", "")
|
||||
|
||||
func main() {
|
||||
err := store.Init()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
for _, filepath := range os.Args[1:] {
|
||||
name := strings.Replace(path.Base(filepath), ".txt", "", 1)
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
log.Println(filepath, err)
|
||||
continue
|
||||
}
|
||||
logLines := parseFile(file)
|
||||
file.Close()
|
||||
|
||||
// Get title and event
|
||||
title := ""
|
||||
event := ""
|
||||
for _, line := range logLines {
|
||||
if line.Verb == "TITLE" {
|
||||
title = line.Text
|
||||
}
|
||||
if line.Verb == "TAG" {
|
||||
event = line.Text
|
||||
}
|
||||
}
|
||||
|
||||
date, channel, err := parseFilename(name)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
l, err := logs.Add(date, channel, title, event, "", false)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = posts.Add(l, time.Now(), "annotation.info", "rpdata-lb2logimport", "This logfile is imported from aitelogs2 and may contain errors or wrong timestamps.")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
for _, line := range logLines {
|
||||
if line.Verb != "CHARS" {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = posts.Add(l, l.Date, "chars", prefixReplacer.Replace(line.Args[0]), line.Text)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, line := range logLines {
|
||||
if line.Verb != "SCENE" && line.Verb != "ACTION" && line.Verb != "TEXT" {
|
||||
continue
|
||||
}
|
||||
|
||||
postTime, err := time.ParseInLocation("2006-01-02 15:04:05", date.Format("2006-01-02")+" "+line.Args[1], time.Local)
|
||||
diff := postTime.Sub(date)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
if diff < 0 {
|
||||
if diff > -time.Second {
|
||||
postTime = postTime.Add(diff)
|
||||
} else {
|
||||
postTime = postTime.Add(time.Hour * 24)
|
||||
}
|
||||
}
|
||||
|
||||
if line.Args[0][0] == '=' {
|
||||
line.Verb = "SCENE"
|
||||
}
|
||||
|
||||
_, err = posts.Add(l, postTime, strings.ToLower(line.Verb), prefixReplacer.Replace(line.Args[0]), line.Text)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
l, err = logs.UpdateCharacters(l, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
fmt.Println(l.ID, "completed")
|
||||
}
|
||||
}
|
||||
+38
-21
@@ -3,52 +3,71 @@ 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/auth"
|
||||
"git.aiterp.net/rpdata/api/internal/config"
|
||||
"git.aiterp.net/rpdata/api/internal/instrumentation"
|
||||
"git.aiterp.net/rpdata/api/internal/loader"
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
"git.aiterp.net/rpdata/api/services"
|
||||
"github.com/99designs/gqlgen/handler"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := store.Init()
|
||||
cfg := config.Global()
|
||||
|
||||
db, err := database.Init(cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to init store:", err)
|
||||
log.Fatalln("Failed to init db:", err)
|
||||
}
|
||||
defer db.Close(context.Background())
|
||||
|
||||
var spaceClient *space.Client
|
||||
if cfg.Space.Enabled {
|
||||
spaceClient, err = space.Connect(cfg.Space)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to init space:", err)
|
||||
}
|
||||
|
||||
log.Println("Space loaded")
|
||||
} else {
|
||||
log.Println("Space is disabled, file upload will not work!")
|
||||
}
|
||||
|
||||
serviceBundle := services.NewBundle(db, spaceClient)
|
||||
|
||||
instrumentation.Register()
|
||||
|
||||
http.Handle("/", handler.Playground("RPData API", "/graphql"))
|
||||
http.Handle("/graphql", queryHandler())
|
||||
http.Handle("/graphql", queryHandler(serviceBundle))
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
go func() {
|
||||
err := logs.RunFullUpdate()
|
||||
err := serviceBundle.Logs.RefreshAllLogCharacters(context.Background())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
log.Println("Refresh Characters:", err)
|
||||
}
|
||||
|
||||
log.Println("Characters updated")
|
||||
err = serviceBundle.Logs.FixImportDateBug(context.Background())
|
||||
if err != nil {
|
||||
log.Println("Fix date bug:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go logListedChanges()
|
||||
go logListedChanges(serviceBundle.Changes)
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8081", nil))
|
||||
}
|
||||
|
||||
func logListedChanges() {
|
||||
sub := changes.Subscribe(context.Background(), nil, true)
|
||||
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)
|
||||
@@ -77,9 +96,11 @@ func logListedChanges() {
|
||||
log.Println("Change subscription closed.")
|
||||
}
|
||||
|
||||
func queryHandler() http.HandlerFunc {
|
||||
func queryHandler(services *services.Bundle) http.HandlerFunc {
|
||||
handler := handler.GraphQL(
|
||||
graph2.New(),
|
||||
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()))
|
||||
@@ -90,11 +111,7 @@ func queryHandler() http.HandlerFunc {
|
||||
handler.RequestMiddleware(instrumentation.RequestMiddleware()),
|
||||
handler.ResolverMiddleware(instrumentation.ResolverMiddleware()),
|
||||
)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := loader.New()
|
||||
r = r.WithContext(l.ToContext(r.Context()))
|
||||
|
||||
// >_>
|
||||
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer of the curse") {
|
||||
w.Header().Set("X-Emerald-Herald", "Seek souls. Larger, more powerful souls.")
|
||||
@@ -102,7 +119,7 @@ func queryHandler() http.HandlerFunc {
|
||||
w.Header().Add("X-Emerald-Herald", "Lest this land swallow you whole... As it has so many others.")
|
||||
}
|
||||
|
||||
r = auth.RequestWithToken(r)
|
||||
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/models/files"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := http.Client{Timeout: time.Second * 30}
|
||||
|
||||
res, err := client.Get(config.Global().Wiki.URL + "?action=query&list=allimages&ailimit=5&aiprop=dimensions|mime|url|user|timestamp&format=json&ailimit=500")
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
log.Fatalln(res.Status)
|
||||
}
|
||||
|
||||
data := ResponseData{}
|
||||
err = json.NewDecoder(res.Body).Decode(&data)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
store.Init()
|
||||
|
||||
for _, info := range data.Query.Allimages {
|
||||
existing, err := files.FindName("wiki", info.Name, info.User)
|
||||
if err == nil && existing.ID != "" {
|
||||
log.Println("Skipping", info.Name, "because it already exists")
|
||||
continue
|
||||
}
|
||||
|
||||
ts, err := time.Parse(time.RFC3339, info.Timestamp)
|
||||
if err != nil {
|
||||
log.Println("Skipping", info.Name, "error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
file, err := files.Insert(info.Name, "wiki", info.Mime, info.User, ts, info.Size, info.URL)
|
||||
if err != nil {
|
||||
log.Println("Skipping", info.Name, "error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println(file.Name, "inserted ( id:", file.ID, ")")
|
||||
}
|
||||
}
|
||||
|
||||
// The ResponseData from the wiki looks like this
|
||||
type ResponseData struct {
|
||||
Batchcomplete string `json:"batchcomplete"`
|
||||
Query struct {
|
||||
Allimages []struct {
|
||||
Descriptionshorturl string `json:"descriptionshorturl"`
|
||||
Descriptionurl string `json:"descriptionurl"`
|
||||
Height int `json:"height"`
|
||||
Mime string `json:"mime"`
|
||||
Name string `json:"name"`
|
||||
Ns int `json:"ns"`
|
||||
Size int64 `json:"size"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
User string `json:"user"`
|
||||
Width int `json:"width"`
|
||||
} `json:"allimages"`
|
||||
} `json:"query"`
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/database/mongodb"
|
||||
"git.aiterp.net/rpdata/api/internal/config"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
)
|
||||
|
||||
// ErrDriverUnrecognized is returned if the driver is not recognized
|
||||
var ErrDriverUnrecognized = errors.New("driver not recognized, check installed version or your configuration")
|
||||
|
||||
type Database interface {
|
||||
Changes() repositories.ChangeRepository
|
||||
Channels() repositories.ChannelRepository
|
||||
Characters() repositories.CharacterRepository
|
||||
Tags() repositories.TagRepository
|
||||
Logs() repositories.LogRepository
|
||||
Posts() repositories.PostRepository
|
||||
Stories() repositories.StoryRepository
|
||||
Chapters() repositories.ChapterRepository
|
||||
Comments() repositories.CommentRepository
|
||||
Keys() repositories.KeyRepository
|
||||
Users() repositories.UserRepository
|
||||
Files() repositories.FileRepository
|
||||
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Init sets up the database.
|
||||
func Init(config config.Database) (Database, error) {
|
||||
switch config.Driver {
|
||||
case "mongo", "mgo", "mongodb":
|
||||
return mongodb.Init(config)
|
||||
default:
|
||||
return nil, ErrDriverUnrecognized
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type changeRepository struct {
|
||||
changes *mgo.Collection
|
||||
idCounter *counter
|
||||
}
|
||||
|
||||
func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) {
|
||||
change := new(models.Change)
|
||||
err := r.changes.FindId(id).One(change)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return change, nil
|
||||
}
|
||||
|
||||
func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) {
|
||||
query := bson.M{}
|
||||
limit := 0
|
||||
if filter.EarliestDate != nil && !filter.EarliestDate.IsZero() {
|
||||
query["date"] = bson.M{"$gte": *filter.EarliestDate}
|
||||
}
|
||||
if len(filter.Keys) > 0 {
|
||||
query["keys"] = bson.M{"$in": filter.Keys}
|
||||
} else {
|
||||
query["listed"] = true
|
||||
}
|
||||
if filter.Author != nil && *filter.Author != "" {
|
||||
query["author"] = *filter.Author
|
||||
}
|
||||
if filter.Limit != nil {
|
||||
limit = *filter.Limit
|
||||
}
|
||||
|
||||
initialSize := 64
|
||||
if limit > 0 && limit < 256 {
|
||||
initialSize = limit
|
||||
}
|
||||
|
||||
changes := make([]*models.Change, 0, initialSize)
|
||||
err := r.changes.Find(query).Sort("-date").Limit(limit).All(&changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) {
|
||||
next, err := r.idCounter.Increment(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
change.ID = "Change_" + strconv.Itoa(next)
|
||||
|
||||
err = r.changes.Insert(&change)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &change, nil
|
||||
}
|
||||
|
||||
func (r *changeRepository) Remove(ctx context.Context, change models.Change) error {
|
||||
return r.changes.RemoveId(change.ID)
|
||||
}
|
||||
|
||||
func newChangeRepository(db *mgo.Database) (repositories.ChangeRepository, error) {
|
||||
collection := db.C("common.changes")
|
||||
|
||||
// Delete the old index if it exists.
|
||||
_ = collection.DropIndexName("date_1")
|
||||
|
||||
err := collection.EnsureIndex(mgo.Index{
|
||||
Name: "expiry",
|
||||
Key: []string{"date"},
|
||||
ExpireAfter: time.Hour * 2400, // 100 days
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndexKey("keys")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &changeRepository{
|
||||
changes: collection,
|
||||
idCounter: newCounter(db, "auto_increment", "Change"),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
func newChannelRepository(db *mgo.Database) (*channelRepository, error) {
|
||||
collection := db.C("common.channels")
|
||||
|
||||
err := collection.EnsureIndexKey("logged")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("event")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("location")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &channelRepository{
|
||||
channels: collection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type channelRepository struct {
|
||||
channels *mgo.Collection
|
||||
}
|
||||
|
||||
func (r *channelRepository) Find(ctx context.Context, name string) (*models.Channel, error) {
|
||||
channel := new(models.Channel)
|
||||
err := r.channels.FindId(name).One(channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (r *channelRepository) List(ctx context.Context, filter models.ChannelFilter) ([]*models.Channel, error) {
|
||||
query := bson.M{}
|
||||
if filter.Logged != nil {
|
||||
query["logged"] = *filter.Logged
|
||||
}
|
||||
if filter.EventName != nil {
|
||||
query["eventName"] = *filter.EventName
|
||||
}
|
||||
if filter.LocationName != nil {
|
||||
query["locationName"] = *filter.LocationName
|
||||
}
|
||||
if len(filter.Names) > 0 {
|
||||
query["_id"] = bson.M{"$in": filter.Names}
|
||||
}
|
||||
|
||||
channels := make([]*models.Channel, 0, 32)
|
||||
err := r.channels.Find(query).Limit(filter.Limit).Sort("_id").All(&channels)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (r *channelRepository) Insert(ctx context.Context, channel models.Channel) (*models.Channel, error) {
|
||||
err := r.channels.Insert(channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *channelRepository) Update(ctx context.Context, channel models.Channel, update models.ChannelUpdate) (*models.Channel, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Logged != nil {
|
||||
updateBson["logged"] = *update.Logged
|
||||
channel.Logged = *update.Logged
|
||||
}
|
||||
if update.LocationName != nil {
|
||||
updateBson["locationName"] = *update.LocationName
|
||||
channel.LocationName = *update.LocationName
|
||||
}
|
||||
if update.EventName != nil {
|
||||
updateBson["eventName"] = *update.EventName
|
||||
channel.EventName = *update.EventName
|
||||
}
|
||||
|
||||
err := r.channels.UpdateId(channel.Name, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *channelRepository) Remove(ctx context.Context, channel models.Channel) error {
|
||||
return r.channels.RemoveId(channel.Name)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"log"
|
||||
)
|
||||
|
||||
type chapterRepository struct {
|
||||
chapters *mgo.Collection
|
||||
comments *mgo.Collection
|
||||
}
|
||||
|
||||
func newChapterRepository(db *mgo.Database) (repositories.ChapterRepository, error) {
|
||||
collection := db.C("story.chapters")
|
||||
|
||||
err := collection.EnsureIndexKey("storyId")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("createdDate")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chapterRepository{
|
||||
chapters: collection,
|
||||
comments: db.C("story.comments"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Find(ctx context.Context, id string) (*models.Chapter, error) {
|
||||
chapter := new(models.Chapter)
|
||||
err := r.chapters.FindId(id).One(chapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chapter, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) {
|
||||
query := bson.M{}
|
||||
if filter.StoryID != nil {
|
||||
query["storyId"] = *filter.StoryID
|
||||
}
|
||||
|
||||
chapters := make([]*models.Chapter, 0, 32)
|
||||
err := r.chapters.Find(query).Sort("createdDate").Limit(filter.Limit).All(&chapters)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return chapters, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chapters, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) {
|
||||
chapter.ID = generate.StoryID()
|
||||
|
||||
err := r.chapters.Insert(chapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Title != nil {
|
||||
updateBson["title"] = *update.Title
|
||||
chapter.Title = *update.Title
|
||||
}
|
||||
if update.Source != nil {
|
||||
updateBson["source"] = *update.Source
|
||||
chapter.Source = *update.Source
|
||||
}
|
||||
if update.FictionalDate != nil {
|
||||
updateBson["fictionalDate"] = *update.FictionalDate
|
||||
chapter.FictionalDate = *update.FictionalDate
|
||||
}
|
||||
if update.CommentMode != nil {
|
||||
updateBson["commentMode"] = *update.CommentMode
|
||||
chapter.CommentMode = *update.CommentMode
|
||||
}
|
||||
if update.CommentsLocked != nil {
|
||||
updateBson["commentsLocked"] = *update.CommentsLocked
|
||||
chapter.CommentsLocked = *update.CommentsLocked
|
||||
}
|
||||
|
||||
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Move(ctx context.Context, chapter models.Chapter, from, to models.Story) (*models.Chapter, error) {
|
||||
err := r.chapters.UpdateId(chapter.ID, bson.M{"$set": bson.M{"storyId": to.ID}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chapter.StoryID = to.ID
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *chapterRepository) Delete(ctx context.Context, chapter models.Chapter) error {
|
||||
err := r.chapters.RemoveId(chapter.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := r.comments.RemoveAll(bson.M{"chapterId": chapter.ID})
|
||||
if err != nil {
|
||||
log.Println("Failed to remove comments:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Removed chapter %s (%d comments)", chapter.ID, c.Removed)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
)
|
||||
|
||||
type characterRepository struct {
|
||||
characters *mgo.Collection
|
||||
cidCounter *counter
|
||||
}
|
||||
|
||||
func newCharacterRepository(db *mgo.Database) (repositories.CharacterRepository, error) {
|
||||
collection := db.C("common.characters")
|
||||
|
||||
err := collection.EnsureIndexKey("name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndexKey("shortName")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"nicks"},
|
||||
Unique: true,
|
||||
DropDups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collection.EnsureIndex(mgo.Index{
|
||||
Key: []string{"$text:description"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &characterRepository{
|
||||
characters: collection,
|
||||
cidCounter: newCounter(db, "auto_increment", "Character"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) Find(ctx context.Context, id string) (*models.Character, error) {
|
||||
character := new(models.Character)
|
||||
err := r.characters.FindId(id).One(character)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) FindNick(ctx context.Context, nick string) (*models.Character, error) {
|
||||
character := new(models.Character)
|
||||
err := r.characters.Find(bson.M{"nick": nick}).One(character)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) FindName(ctx context.Context, name string) (*models.Character, error) {
|
||||
query := bson.M{
|
||||
"$or": []bson.M{
|
||||
{"shortName": name},
|
||||
{"name": name},
|
||||
},
|
||||
}
|
||||
|
||||
// Look for all characters matching query
|
||||
characters := make([]*models.Character, 0, 8)
|
||||
err := r.characters.Find(query).All(&characters)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
} else if len(characters) == 0 {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
// Prioritize exact match
|
||||
for _, character := range characters {
|
||||
if character.Name == name {
|
||||
return character, nil
|
||||
}
|
||||
}
|
||||
|
||||
return characters[0], nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) List(ctx context.Context, filter models.CharacterFilter) ([]*models.Character, error) {
|
||||
query := bson.M{}
|
||||
if filter.Author != nil {
|
||||
query["author"] = *filter.Author
|
||||
}
|
||||
if len(filter.IDs) > 0 {
|
||||
query["_id"] = bson.M{"$in": filter.IDs}
|
||||
}
|
||||
if len(filter.Nicks) > 0 {
|
||||
query["nicks"] = bson.M{"$in": filter.Nicks}
|
||||
}
|
||||
if len(filter.Names) > 0 {
|
||||
query["$or"] = []bson.M{
|
||||
{"name": bson.M{"$in": filter.Names}},
|
||||
{"shortName": bson.M{"$in": filter.Names}},
|
||||
}
|
||||
}
|
||||
if filter.Search != nil {
|
||||
query["$text"] = bson.M{"$search": *filter.Search}
|
||||
}
|
||||
|
||||
characters := make([]*models.Character, 0, 32)
|
||||
err := r.characters.Find(query).Limit(filter.Limit).All(&characters)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return characters, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(characters, func(i, j int) bool {
|
||||
ni, _ := strconv.Atoi(characters[i].ID[1:])
|
||||
nj, _ := strconv.Atoi(characters[j].ID[1:])
|
||||
|
||||
return ni < nj
|
||||
})
|
||||
|
||||
return characters, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) Insert(ctx context.Context, character models.Character) (*models.Character, error) {
|
||||
nextId, err := r.cidCounter.Increment(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
character.ID = "C" + strconv.Itoa(nextId)
|
||||
|
||||
err = r.characters.Insert(&character)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) Update(ctx context.Context, character models.Character, update models.CharacterUpdate) (*models.Character, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Name != nil {
|
||||
updateBson["name"] = *update.Name
|
||||
character.Name = *update.Name
|
||||
}
|
||||
if update.ShortName != nil {
|
||||
updateBson["shortName"] = *update.ShortName
|
||||
character.ShortName = *update.ShortName
|
||||
}
|
||||
if update.Description != nil {
|
||||
updateBson["description"] = *update.Description
|
||||
character.Description = *update.Description
|
||||
}
|
||||
|
||||
err := r.characters.UpdateId(character.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) AddNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
|
||||
if character.HasNick(nick) {
|
||||
return nil, errors.New("nick already exist")
|
||||
}
|
||||
|
||||
match := bson.M{
|
||||
"_id": character.ID,
|
||||
"nicks": bson.M{"$ne": nick},
|
||||
}
|
||||
|
||||
err := r.characters.Update(match, bson.M{"$push": bson.M{"nicks": nick}})
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
|
||||
copy(newNicks, character.Nicks)
|
||||
character.Nicks = append(newNicks, nick)
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) RemoveNick(ctx context.Context, character models.Character, nick string) (*models.Character, error) {
|
||||
if !character.HasNick(nick) {
|
||||
return nil, errors.New("nick does not exist")
|
||||
}
|
||||
|
||||
match := bson.M{
|
||||
"_id": character.ID,
|
||||
"nicks": nick,
|
||||
}
|
||||
|
||||
err := r.characters.Update(match, bson.M{"$pull": bson.M{"nicks": nick}})
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
} else if mErr, ok := err.(*mgo.LastError); ok && mErr.Code == 11000 {
|
||||
return nil, errors.New("The nick belongs to another character already")
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newNicks := make([]string, len(character.Nicks), len(character.Nicks)+1)
|
||||
copy(newNicks, character.Nicks)
|
||||
for i := range newNicks {
|
||||
if newNicks[i] == nick {
|
||||
newNicks = append(newNicks[:i], newNicks[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
character.Nicks = newNicks
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *characterRepository) Delete(ctx context.Context, character models.Character) error {
|
||||
return r.characters.RemoveId(character.ID)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"log"
|
||||
)
|
||||
|
||||
type commentRepository struct {
|
||||
comments *mgo.Collection
|
||||
}
|
||||
|
||||
func newCommentRepository(db *mgo.Database) (repositories.CommentRepository, error) {
|
||||
collection := db.C("story.comments")
|
||||
|
||||
err := collection.EnsureIndexKey("chapterId")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = collection.EnsureIndexKey("createdDate")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &commentRepository{
|
||||
comments: collection,
|
||||
}
|
||||
|
||||
go r.fixFieldTypo()
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) Find(ctx context.Context, id string) (*models.Comment, error) {
|
||||
comment := new(models.Comment)
|
||||
err := r.comments.FindId(id).One(comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) List(ctx context.Context, filter models.CommentFilter) ([]*models.Comment, error) {
|
||||
query := bson.M{}
|
||||
if filter.ChapterID != nil {
|
||||
query["chapterId"] = *filter.ChapterID
|
||||
}
|
||||
|
||||
comments := make([]*models.Comment, 0, 32)
|
||||
err := r.comments.Find(query).Sort("createdDate").Limit(filter.Limit).All(&comments)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) Insert(ctx context.Context, comment models.Comment) (*models.Comment, error) {
|
||||
comment.ID = generate.CommentID()
|
||||
|
||||
err := r.comments.Insert(comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) Update(ctx context.Context, comment models.Comment, update models.CommentUpdate) (*models.Comment, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Subject != nil {
|
||||
updateBson["subject"] = *update.Subject
|
||||
comment.Subject = *update.Subject
|
||||
}
|
||||
if update.Source != nil {
|
||||
updateBson["source"] = *update.Source
|
||||
comment.Source = *update.Source
|
||||
}
|
||||
if update.FictionalDate != nil {
|
||||
updateBson["fictionalDate"] = *update.FictionalDate
|
||||
comment.FictionalDate = *update.FictionalDate
|
||||
}
|
||||
if update.CharacterID != nil {
|
||||
updateBson["characterId"] = *update.CharacterID
|
||||
comment.CharacterID = *update.CharacterID
|
||||
}
|
||||
if update.CharacterName != nil {
|
||||
updateBson["characterName"] = *update.CharacterName
|
||||
comment.CharacterName = *update.CharacterName
|
||||
}
|
||||
|
||||
err := r.comments.UpdateId(comment.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
func (r *commentRepository) Delete(ctx context.Context, comment models.Comment) error {
|
||||
return r.comments.RemoveId(comment.ID)
|
||||
}
|
||||
|
||||
func (r *commentRepository) fixFieldTypo() {
|
||||
c, err := r.comments.UpdateAll(bson.M{
|
||||
"editeddDate": bson.M{"$ne": nil},
|
||||
}, bson.M{
|
||||
"$rename": bson.M{"editeddDate": "editedDate"},
|
||||
})
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Failed to run name typo fix:", err)
|
||||
return
|
||||
}
|
||||
if c.Updated > 0 {
|
||||
log.Println("Fixed editeddDate field name typo in", c.Updated, "comments")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/config"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
type MongoDB struct {
|
||||
session *mgo.Session
|
||||
|
||||
changes repositories.ChangeRepository
|
||||
channels *channelRepository
|
||||
characters repositories.CharacterRepository
|
||||
tags repositories.TagRepository
|
||||
logs *logRepository
|
||||
posts *postRepository
|
||||
files *fileRepository
|
||||
stories repositories.StoryRepository
|
||||
chapters repositories.ChapterRepository
|
||||
comments repositories.CommentRepository
|
||||
keys repositories.KeyRepository
|
||||
users repositories.UserRepository
|
||||
}
|
||||
|
||||
func (m *MongoDB) Changes() repositories.ChangeRepository {
|
||||
return m.changes
|
||||
}
|
||||
|
||||
func (m *MongoDB) Characters() repositories.CharacterRepository {
|
||||
return m.characters
|
||||
}
|
||||
|
||||
func (m *MongoDB) Tags() repositories.TagRepository {
|
||||
return m.tags
|
||||
}
|
||||
|
||||
func (m *MongoDB) Logs() repositories.LogRepository {
|
||||
return m.logs
|
||||
}
|
||||
|
||||
func (m *MongoDB) Posts() repositories.PostRepository {
|
||||
return m.posts
|
||||
}
|
||||
|
||||
func (m *MongoDB) Channels() repositories.ChannelRepository {
|
||||
return m.channels
|
||||
}
|
||||
|
||||
func (m *MongoDB) Files() repositories.FileRepository {
|
||||
return m.files
|
||||
}
|
||||
|
||||
func (m *MongoDB) Stories() repositories.StoryRepository {
|
||||
return m.stories
|
||||
}
|
||||
|
||||
func (m *MongoDB) Chapters() repositories.ChapterRepository {
|
||||
return m.chapters
|
||||
}
|
||||
|
||||
func (m *MongoDB) Comments() repositories.CommentRepository {
|
||||
return m.comments
|
||||
}
|
||||
|
||||
func (m *MongoDB) Keys() repositories.KeyRepository {
|
||||
return m.keys
|
||||
}
|
||||
|
||||
func (m *MongoDB) Users() repositories.UserRepository {
|
||||
return m.users
|
||||
}
|
||||
|
||||
func (m *MongoDB) Close(ctx context.Context) error {
|
||||
m.session.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initializes the mongodb database
|
||||
func Init(cfg config.Database) (*MongoDB, error) {
|
||||
port := cfg.Port
|
||||
if port <= 0 {
|
||||
port = 27017
|
||||
}
|
||||
|
||||
session, err := mgo.DialWithInfo(&mgo.DialInfo{
|
||||
Addrs: []string{fmt.Sprintf("%s:%d", cfg.Host, port)},
|
||||
Timeout: 30 * time.Second,
|
||||
Database: cfg.Db,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
Mechanism: cfg.Mechanism,
|
||||
Source: cfg.Db,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := session.DB(cfg.Db)
|
||||
|
||||
characters, err := newCharacterRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels, err := newChannelRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changes, err := newChangeRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs, err := newLogRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
posts, err := newPostRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := newFileRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stories, err := newStoryRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chapters, err := newChapterRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comments, err := newCommentRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys, err := newKeyRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users, err := newUserRepository(db)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go posts.fixPositions(logs)
|
||||
|
||||
return &MongoDB{
|
||||
session: session,
|
||||
|
||||
changes: changes,
|
||||
characters: characters,
|
||||
channels: channels,
|
||||
tags: newTagRepository(db),
|
||||
stories: stories,
|
||||
chapters: chapters,
|
||||
comments: comments,
|
||||
logs: logs,
|
||||
posts: posts,
|
||||
files: files,
|
||||
keys: keys,
|
||||
users: users,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type counter struct {
|
||||
coll *mgo.Collection
|
||||
category string
|
||||
name string
|
||||
}
|
||||
|
||||
func newCounter(db *mgo.Database, category, name string) *counter {
|
||||
return &counter{
|
||||
coll: db.C("core.counters"),
|
||||
category: category,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *counter) WithName(name string) *counter {
|
||||
return &counter{
|
||||
coll: c.coll,
|
||||
category: c.category,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *counter) WithCategory(category string) *counter {
|
||||
return &counter{
|
||||
coll: c.coll,
|
||||
category: category,
|
||||
name: c.name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *counter) With(category, name string) *counter {
|
||||
return &counter{
|
||||
coll: c.coll,
|
||||
category: category,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *counter) Increment(amount int) (int, error) {
|
||||
type counterDoc struct {
|
||||
ID string `bson:"_id"`
|
||||
Value int `bson:"value"`
|
||||
}
|
||||
|
||||
id := c.category + "." + c.name
|
||||
doc := counterDoc{}
|
||||
|
||||
_, err := c.coll.Find(bson.M{"_id": id}).Apply(mgo.Change{
|
||||
Update: bson.M{"$inc": bson.M{"value": amount}},
|
||||
Upsert: true,
|
||||
ReturnNew: true,
|
||||
}, &doc)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return doc.Value, nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
type fileRepository struct {
|
||||
files *mgo.Collection
|
||||
}
|
||||
|
||||
func (r *fileRepository) Find(ctx context.Context, id string) (*models.File, error) {
|
||||
file := new(models.File)
|
||||
err := r.files.FindId(id).One(file)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) {
|
||||
query := bson.M{}
|
||||
if filter.Author != nil {
|
||||
query["author"] = *filter.Author
|
||||
}
|
||||
if filter.MimeTypes != nil {
|
||||
query["mimeType"] = bson.M{"$in": filter.MimeTypes}
|
||||
}
|
||||
if filter.Public != nil {
|
||||
query["public"] = *filter.Public
|
||||
}
|
||||
|
||||
files := make([]*models.File, 0, 16)
|
||||
err := r.files.Find(query).Sort("-date").All(&files)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) Insert(ctx context.Context, file models.File) (*models.File, error) {
|
||||
file.ID = generate.FileID()
|
||||
|
||||
err := r.files.Insert(&file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) Update(ctx context.Context, file models.File, update models.FileUpdate) (*models.File, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Public != nil {
|
||||
updateBson["public"] = *update.Public
|
||||
file.Public = *update.Public
|
||||
}
|
||||
if update.Name != nil {
|
||||
updateBson["name"] = *update.Name
|
||||
file.Name = *update.Name
|
||||
}
|
||||
|
||||
err := r.files.UpdateId(file.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *fileRepository) Delete(ctx context.Context, file models.File) error {
|
||||
return r.files.RemoveId(file.ID)
|
||||
}
|
||||
|
||||
func newFileRepository(db *mgo.Database) (*fileRepository, error) {
|
||||
files := db.C("file.headers")
|
||||
|
||||
err := files.EnsureIndexKey("author")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = files.EnsureIndexKey("mimeType")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = files.EnsureIndexKey("public")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &fileRepository{files: files}, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
type keyRepository struct {
|
||||
keys *mgo.Collection
|
||||
}
|
||||
|
||||
func newKeyRepository(db *mgo.Database) (repositories.KeyRepository, error) {
|
||||
collection := db.C("auth.keys")
|
||||
|
||||
err := collection.EnsureIndexKey("user")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &keyRepository{keys: collection}, nil
|
||||
}
|
||||
|
||||
func (r *keyRepository) Find(ctx context.Context, id string) (*models.Key, error) {
|
||||
key := models.Key{}
|
||||
err := r.keys.FindId(id).One(&key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &key, err
|
||||
}
|
||||
|
||||
func (r *keyRepository) List(ctx context.Context, filter models.KeyFilter) ([]*models.Key, error) {
|
||||
query := bson.M{}
|
||||
if filter.UserID != nil {
|
||||
query["user"] = *filter.UserID
|
||||
}
|
||||
|
||||
keys := make([]*models.Key, 0, 4)
|
||||
err := r.keys.Find(query).All(&keys)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (r *keyRepository) Insert(ctx context.Context, key models.Key) (*models.Key, error) {
|
||||
key.ID = generate.KeyID()
|
||||
|
||||
err := r.keys.Insert(&key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func (r *keyRepository) Delete(ctx context.Context, key models.Key) error {
|
||||
return r.keys.RemoveId(key.ID)
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type logRepository struct {
|
||||
openMutex sync.Mutex
|
||||
|
||||
logs *mgo.Collection
|
||||
posts *mgo.Collection
|
||||
shortIdCounter *counter
|
||||
}
|
||||
|
||||
func newLogRepository(db *mgo.Database) (*logRepository, error) {
|
||||
logs := db.C("logbot3.logs")
|
||||
posts := db.C("logbot3.posts")
|
||||
|
||||
err := logs.EnsureIndexKey("date")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = logs.EnsureIndexKey("channel")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = logs.EnsureIndexKey("characterIds")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = logs.EnsureIndexKey("event")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = logs.EnsureIndex(mgo.Index{Key: []string{"channel", "open"}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = logs.EnsureIndex(mgo.Index{
|
||||
Key: []string{"shortId"},
|
||||
Unique: true,
|
||||
DropDups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &logRepository{
|
||||
logs: logs,
|
||||
posts: posts,
|
||||
shortIdCounter: newCounter(db, "auto_increment", "Log"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) Find(ctx context.Context, id string) (*models.Log, error) {
|
||||
log := new(models.Log)
|
||||
err := r.logs.Find(bson.M{"$or": []bson.M{{"_id": id}, {"shortId": id}}}).One(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error) {
|
||||
query := bson.M{}
|
||||
if filter.Search != nil {
|
||||
searchQuery := bson.M{
|
||||
"$text": bson.M{"$search": *filter.Search},
|
||||
"logId": bson.M{"$ne": nil},
|
||||
}
|
||||
|
||||
logIds := make([]string, 0, 64)
|
||||
err := r.posts.Find(searchQuery).Distinct("logId", &logIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query["shortId"] = bson.M{"$in": logIds}
|
||||
}
|
||||
if filter.Open != nil {
|
||||
r.openMutex.Lock()
|
||||
defer r.openMutex.Unlock()
|
||||
|
||||
query["open"] = filter.Open
|
||||
}
|
||||
if len(filter.Characters) > 0 {
|
||||
query["characterIds"] = bson.M{"$all": filter.Characters}
|
||||
}
|
||||
if len(filter.Channels) > 0 {
|
||||
query["channel"] = bson.M{"$in": filter.Channels}
|
||||
}
|
||||
if len(filter.Events) > 0 {
|
||||
query["event"] = bson.M{"$in": filter.Events}
|
||||
}
|
||||
if filter.MinDate != nil && filter.MaxDate != nil {
|
||||
query["date"] = bson.M{"$gte": *filter.MinDate, "$lte": *filter.MaxDate}
|
||||
} else if filter.MinDate != nil {
|
||||
query["date"] = bson.M{"$gte": *filter.MinDate}
|
||||
} else if filter.MaxDate != nil {
|
||||
query["date"] = bson.M{"$lt": *filter.MaxDate}
|
||||
}
|
||||
|
||||
logs := make([]*models.Log, 0, 32)
|
||||
err := r.logs.Find(query).Sort("-date").Limit(filter.Limit).All(&logs)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) Insert(ctx context.Context, log models.Log) (*models.Log, error) {
|
||||
nextShortId, err := r.shortIdCounter.Increment(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.ID = generate.LogID(log)
|
||||
log.ShortID = "L" + strconv.Itoa(nextShortId)
|
||||
|
||||
if log.Open {
|
||||
// There can be only one open log in the same channel.
|
||||
r.openMutex.Lock()
|
||||
defer r.openMutex.Unlock()
|
||||
|
||||
_, err = r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
|
||||
if err != nil {
|
||||
return nil, errors.New("Cannot close other logs: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err = r.logs.Insert(&log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &log, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) Update(ctx context.Context, log models.Log, update models.LogUpdate) (*models.Log, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Open != nil {
|
||||
if *update.Open == true {
|
||||
// There can be only one open log in the same channel.
|
||||
r.openMutex.Lock()
|
||||
defer r.openMutex.Unlock()
|
||||
|
||||
_, err := r.logs.UpdateAll(bson.M{"channel": log.ChannelName, "open": true}, bson.M{"$set": bson.M{"open": false}})
|
||||
if err != nil {
|
||||
return nil, errors.New("Cannot close other logs: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
updateBson["open"] = *update.Open
|
||||
log.Open = *update.Open
|
||||
}
|
||||
if update.Title != nil {
|
||||
updateBson["title"] = *update.Title
|
||||
log.Title = *update.Title
|
||||
}
|
||||
if update.Description != nil {
|
||||
updateBson["description"] = *update.Description
|
||||
log.Description = *update.Description
|
||||
}
|
||||
if update.EventName != nil {
|
||||
updateBson["event"] = *update.EventName
|
||||
log.EventName = *update.EventName
|
||||
}
|
||||
if update.CharacterIDs != nil {
|
||||
updateBson["characterIds"] = update.CharacterIDs
|
||||
log.CharacterIDs = update.CharacterIDs
|
||||
}
|
||||
|
||||
err := r.logs.UpdateId(log.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &log, nil
|
||||
}
|
||||
|
||||
func (r *logRepository) Delete(ctx context.Context, log models.Log) error {
|
||||
err := r.logs.RemoveId(log.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = r.posts.RemoveAll(bson.M{"logId": log.ShortID})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/internal/generate"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type postRepository struct {
|
||||
logs *mgo.Collection
|
||||
posts *mgo.Collection
|
||||
|
||||
orderMutex sync.Mutex
|
||||
}
|
||||
|
||||
func newPostRepository(db *mgo.Database) (*postRepository, error) {
|
||||
posts := db.C("logbot3.posts")
|
||||
|
||||
err := posts.EnsureIndexKey("logId")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = posts.EnsureIndexKey("time")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = posts.EnsureIndexKey("kind")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = posts.EnsureIndexKey("position")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = posts.EnsureIndex(mgo.Index{
|
||||
Key: []string{"$text:text"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &postRepository{
|
||||
posts: posts,
|
||||
logs: db.C("logbot3.logs"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Find(ctx context.Context, id string) (*models.Post, error) {
|
||||
post := new(models.Post)
|
||||
err := r.posts.FindId(id).One(post)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) List(ctx context.Context, filter models.PostFilter) ([]*models.Post, error) {
|
||||
query := bson.M{}
|
||||
if filter.LogID != nil && *filter.LogID != "" {
|
||||
logId := *filter.LogID
|
||||
if !strings.HasPrefix(logId, "L") {
|
||||
// Resolve long id to short id
|
||||
log := new(models.Log)
|
||||
err := r.logs.FindId(logId).Select(bson.M{"logId": 1, "_id": 1}).One(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logId = log.ShortID
|
||||
}
|
||||
|
||||
query["logId"] = logId
|
||||
}
|
||||
if len(filter.IDs) > 0 {
|
||||
query["_id"] = bson.M{"$in": filter.IDs}
|
||||
}
|
||||
if len(filter.Kinds) > 0 {
|
||||
query["kind"] = bson.M{"$in": filter.Kinds}
|
||||
}
|
||||
if filter.Search != nil {
|
||||
query["$text"] = bson.M{"$search": *filter.Search}
|
||||
}
|
||||
|
||||
posts := make([]*models.Post, 0, 32)
|
||||
err := r.posts.Find(query).Sort("-logId", "position").Limit(filter.Limit).All(&posts)
|
||||
if err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return []*models.Post{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Insert(ctx context.Context, post models.Post) (*models.Post, error) {
|
||||
r.orderMutex.Lock()
|
||||
defer r.orderMutex.Unlock()
|
||||
|
||||
lastPost := new(models.Post)
|
||||
err := r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
post.ID = generate.PostID()
|
||||
post.Position = lastPost.Position + 1 // Position 1 is first position, so this is safe.
|
||||
|
||||
err = r.posts.Insert(post)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) InsertMany(ctx context.Context, posts ...*models.Post) ([]*models.Post, error) {
|
||||
if len(posts) == 0 {
|
||||
return []*models.Post{}, nil
|
||||
}
|
||||
|
||||
logId := posts[0].LogID
|
||||
for _, post := range posts {
|
||||
if post.LogID != logId {
|
||||
return nil, repositories.ErrParentMismatch
|
||||
}
|
||||
|
||||
post.ID = generate.PostID()
|
||||
}
|
||||
|
||||
r.orderMutex.Lock()
|
||||
defer r.orderMutex.Unlock()
|
||||
|
||||
lastPost := new(models.Post)
|
||||
err := r.posts.Find(bson.M{"logId": posts[0].LogID}).Sort("-position").One(lastPost)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docs := make([]interface{}, len(posts))
|
||||
for i := range posts {
|
||||
posts[i].Position = lastPost.Position + 1 + i
|
||||
docs[i] = posts[i]
|
||||
}
|
||||
|
||||
err = r.posts.Insert(docs...)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Update(ctx context.Context, post models.Post, update models.PostUpdate) (*models.Post, error) {
|
||||
updateBson := bson.M{}
|
||||
if update.Time != nil {
|
||||
updateBson["time"] = *update.Time
|
||||
post.Time = *update.Time
|
||||
}
|
||||
if update.Kind != nil {
|
||||
updateBson["kind"] = *update.Kind
|
||||
post.Kind = *update.Kind
|
||||
}
|
||||
if update.Nick != nil {
|
||||
updateBson["nick"] = *update.Nick
|
||||
post.Nick = *update.Nick
|
||||
}
|
||||
if update.Text != nil {
|
||||
updateBson["text"] = *update.Text
|
||||
post.Text = *update.Text
|
||||
}
|
||||
|
||||
err := r.posts.UpdateId(post.ID, bson.M{"$set": updateBson})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Move(ctx context.Context, post models.Post, position int) ([]*models.Post, error) {
|
||||
// If only MongoDB transactions weren't awful, this function would have been safe.
|
||||
// Since it isn't, then good luck.
|
||||
|
||||
// Validate lower position bound.
|
||||
if position < 1 {
|
||||
return nil, repositories.ErrInvalidPosition
|
||||
}
|
||||
|
||||
// Determine the operations on adjacent posts
|
||||
var resultFilter bson.M
|
||||
var pushFilter bson.M
|
||||
var increment int
|
||||
if post.Position > position {
|
||||
pushFilter = bson.M{"position": bson.M{
|
||||
"$gte": position,
|
||||
"$lt": post.Position,
|
||||
}}
|
||||
increment = 1
|
||||
resultFilter = bson.M{"position": bson.M{
|
||||
"$lte": post.Position,
|
||||
"$gte": position,
|
||||
}}
|
||||
} else {
|
||||
pushFilter = bson.M{"position": bson.M{
|
||||
"$lte": position,
|
||||
"$gt": post.Position,
|
||||
}}
|
||||
increment = -1
|
||||
resultFilter = bson.M{"position": bson.M{
|
||||
"$lte": position,
|
||||
"$gte": post.Position,
|
||||
}}
|
||||
}
|
||||
pushFilter["logId"] = post.LogID
|
||||
resultFilter["logId"] = post.LogID
|
||||
|
||||
// From here on out, sync is required
|
||||
r.orderMutex.Lock()
|
||||
defer r.orderMutex.Unlock()
|
||||
|
||||
// Detect ninja shenanigans
|
||||
post2 := new(models.Post)
|
||||
err := r.posts.FindId(post.ID).One(post2)
|
||||
if err != nil && post2.Position != post.Position {
|
||||
return nil, repositories.ErrNotFound
|
||||
}
|
||||
|
||||
// Validate upper position bound
|
||||
lastPost := new(models.Post)
|
||||
err = r.posts.Find(bson.M{"logId": post.LogID}).Sort("-position").One(lastPost)
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
if position > lastPost.Position {
|
||||
return nil, repositories.ErrInvalidPosition
|
||||
}
|
||||
|
||||
// Move the posts
|
||||
changeInfo, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": increment}})
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
err = r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": position}})
|
||||
if err != nil {
|
||||
// Try to undo it
|
||||
_, err := r.posts.UpdateAll(pushFilter, bson.M{"$inc": bson.M{"position": -increment}})
|
||||
if err != nil && err != mgo.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]*models.Post, 0, changeInfo.Matched+1)
|
||||
err = r.posts.Find(resultFilter).All(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *postRepository) Delete(ctx context.Context, post models.Post) error {
|
||||
r.orderMutex.Lock()
|
||||
defer r.orderMutex.Unlock()
|
||||
|
||||
err := r.posts.RemoveId(post.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = r.posts.UpdateAll(bson.M{"logId": post.LogID, "position": bson.M{"$gt": post.Position}}, bson.M{"$inc": bson.M{"position": -1}})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *postRepository) fixPositions(logRepo *logRepository) {
|
||||
disorders := make([]int, 0, 16)
|
||||
diffs := make([]int, 0, 16)
|
||||
startTime := time.Now()
|
||||
|
||||
timeout, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
logs, err := logRepo.List(timeout, models.LogFilter{})
|
||||
if err != nil {
|
||||
log.Println("Failed to get logs for position fix:", err)
|
||||
}
|
||||
|
||||
log.Println("Starting log position fixing, this should not take longer than 10 seconds.")
|
||||
|
||||
for _, l := range logs {
|
||||
r.orderMutex.Lock()
|
||||
|
||||
posts, err := r.List(timeout, models.PostFilter{LogID: &l.ShortID})
|
||||
if err != nil {
|
||||
r.orderMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
disorders = disorders[:0]
|
||||
diffs = diffs[:0]
|
||||
for i, post := range posts {
|
||||
if post.Position != (i + 1) {
|
||||
disorders = append(disorders, i)
|
||||
diffs = append(diffs, post.Position-(i+1))
|
||||
}
|
||||
}
|
||||
|
||||
if len(disorders) > 0 {
|
||||
log.Println(len(disorders), "order errors detected in", l.ID)
|
||||
|
||||
ops := 0
|
||||
|
||||
for i, post := range posts {
|
||||
if (i + 1) != posts[i].Position {
|
||||
ops++
|
||||
|
||||
err := r.posts.UpdateId(post.ID, bson.M{"$set": bson.M{"position": i + 1}})
|
||||
if err != nil {
|
||||
log.Println(l.ShortID, "fix failed after", ops, "ops:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Println(l.ShortID, "fixed after", ops, "ops.")
|
||||
}
|
||||
|
||||
r.orderMutex.Unlock()
|
||||
}
|
||||
|
||||
log.Println("Log position fixing finished in", time.Since(startTime))
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -4,38 +4,31 @@ go 1.12
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.9.0
|
||||
github.com/agnivade/levenshtein v1.0.1
|
||||
github.com/beorn7/perks v1.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4
|
||||
github.com/globalsign/mgo v0.0.0-20180403085842-f76e4f9da92e
|
||||
github.com/go-ini/ini v1.35.0
|
||||
github.com/go-sql-driver/mysql v1.4.0
|
||||
github.com/go-ini/ini v1.35.0 // indirect
|
||||
github.com/golang/protobuf v1.3.1 // indirect
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/graph-gophers/dataloader v0.0.0-20180104184831-78139374585c
|
||||
github.com/hashicorp/golang-lru v0.5.0
|
||||
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0
|
||||
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
|
||||
github.com/opentracing/opentracing-go v1.0.2
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
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/sirupsen/logrus v1.2.0
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/urfave/cli v1.20.0
|
||||
github.com/tidwall/pretty v1.0.0 // indirect
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser v1.1.2
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
|
||||
golang.org/x/net v0.0.0-20190514140710-3ec191127204
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a
|
||||
golang.org/x/text v0.3.0
|
||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd
|
||||
google.golang.org/appengine v1.1.0
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
github.com/99designs/gqlgen v0.5.1 h1:cRsbpZgX83PrXb0/hj5EkNdmaVN4l/Eii81Z8LCexgY=
|
||||
github.com/99designs/gqlgen v0.5.1/go.mod h1:KSQDfLlTTGmzlRgLGm6HeKKKo598l5E2svEM6Nz2Jnw=
|
||||
github.com/99designs/gqlgen v0.8.0 h1:ZBteuSgeeFwn+mztjDqQZQdVa9pqslmPnDeCovcnc0Y=
|
||||
github.com/99designs/gqlgen v0.8.0/go.mod h1:st7qHA6ssU3uRZkmv+wzrzgX4srvIqEIdE5iuRW8GhE=
|
||||
github.com/99designs/gqlgen v0.8.3 h1:I6bMglXNKkn4KlvkSMzqZw53e1N2FF9Gud4NmsOxqiA=
|
||||
github.com/99designs/gqlgen v0.8.3/go.mod h1:aLyJw9xUgdJxZ8EqNQxo2pGFhXXJ/hq8t7J4yn8TgI4=
|
||||
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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/agnivade/levenshtein v1.0.0 h1:q+77q31bLT5jhN3BHKA1276nUEdbz7XjDa0o5dRKcZ0=
|
||||
github.com/agnivade/levenshtein v1.0.0/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
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/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
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=
|
||||
@@ -31,31 +21,36 @@ 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-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
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/graph-gophers/dataloader v0.0.0-20180104184831-78139374585c h1:94S+uoVVMpQAEOrqGjCDyUdML4dJDkh6aC4MYmXECg4=
|
||||
github.com/graph-gophers/dataloader v0.0.0-20180104184831-78139374585c/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4=
|
||||
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/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0 h1:5B0uxl2lzNRVkJVg+uGHxWtRt4C0Wjc6kJKo5XYx8xE=
|
||||
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
|
||||
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=
|
||||
@@ -65,7 +60,6 @@ github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ew
|
||||
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/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
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=
|
||||
@@ -73,80 +67,73 @@ 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_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
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.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
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-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
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/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
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.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
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.0/go.mod h1:vxM6NuRlgiR0M6wbVTJeKp9vQIs81ZMfCYO+4yq/jbE=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser v0.0.0-20180831041411-14e83ae06ec1 h1:FYOXUtr3sYR9shto7Q/aQ1B0Onyk77aws9wGOORiz+I=
|
||||
github.com/vektah/gqlparser v0.0.0-20180831041411-14e83ae06ec1/go.mod h1:K4QdSSpS2XiHHwzb18kWh3iBljB8rLC8okGXsnQy3Nc=
|
||||
github.com/vektah/gqlparser v1.1.0 h1:3668p2gUlO+PiS81x957Rpr3/FPRWG6cxgCXAvTS1hw=
|
||||
github.com/vektah/gqlparser v1.1.0/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20180411161317-d6449816ce06 h1:EOqG0JqGlLr+punVB69jvWCv/ErZKGlC7PMdyHfv+Bc=
|
||||
golang.org/x/crypto v0.0.0-20180411161317-d6449816ce06/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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-20180404174746-b3c676e531a6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190514140710-3ec191127204 h1:4yG6GqBtw9C+UrLp6s2wtSniayy/Vd/3F7ffLE427XI=
|
||||
golang.org/x/net v0.0.0-20190514140710-3ec191127204/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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-20180416112224-2f57af4873d0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20180911133044-677d2ff680c1 h1:dzEuQYa6+a3gROnSlgly5ERUm4SZKJt+dh+4iSbO+bI=
|
||||
golang.org/x/tools v0.0.0-20180911133044-677d2ff680c1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
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=
|
||||
|
||||
+50
-46
@@ -1,17 +1,11 @@
|
||||
package graph2
|
||||
|
||||
import (
|
||||
input "git.aiterp.net/rpdata/api/graph2/input"
|
||||
changes "git.aiterp.net/rpdata/api/models/changes"
|
||||
channels "git.aiterp.net/rpdata/api/models/channels"
|
||||
characters "git.aiterp.net/rpdata/api/models/characters"
|
||||
files "git.aiterp.net/rpdata/api/models/files"
|
||||
logs "git.aiterp.net/rpdata/api/models/logs"
|
||||
posts "git.aiterp.net/rpdata/api/models/posts"
|
||||
stories "git.aiterp.net/rpdata/api/models/stories"
|
||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
func complexity() (cr ComplexityRoot) {
|
||||
func complexity() (cr graphcore.ComplexityRoot) {
|
||||
subListComplexity := 25
|
||||
bigSublistComplexity := 75
|
||||
hugeSublistComplexity := 100
|
||||
@@ -22,29 +16,29 @@ func complexity() (cr ComplexityRoot) {
|
||||
cr.Query.Character = func(childComplexity int, id *string, nick *string) int {
|
||||
return childComplexity + findComplexity
|
||||
}
|
||||
cr.Query.Characters = func(childComplexity int, filter *characters.Filter) int {
|
||||
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 *channels.Filter) int {
|
||||
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 *posts.Filter) int {
|
||||
cr.Query.Posts = func(childComplexity int, filter *models.PostFilter) int {
|
||||
return childComplexity + listComplexity
|
||||
}
|
||||
cr.Query.UnknownNicks = func(childComplexity int, filter *input.UnknownNicksFilter) int {
|
||||
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 *logs.Filter) int {
|
||||
if filter != nil && filter.Open != nil && *filter.Open == true {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -56,107 +50,111 @@ func complexity() (cr ComplexityRoot) {
|
||||
cr.Query.Comment = func(childComplexity int, id string) int {
|
||||
return childComplexity + findComplexity
|
||||
}
|
||||
cr.Query.Tags = func(childComplexity int) int {
|
||||
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 *stories.Filter) int {
|
||||
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 *files.Filter) int {
|
||||
cr.Query.Files = func(childComplexity int, filter *models.FileFilter) int {
|
||||
return childComplexity + listComplexity
|
||||
}
|
||||
cr.Query.Changes = func(childComplexity int, filter *changes.Filter) int {
|
||||
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 input.StoryAddInput) int {
|
||||
cr.Mutation.AddStory = func(childComplexity int, input graphcore.StoryAddInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.AddStoryTag = func(childComplexity int, input input.StoryTagAddInput) int {
|
||||
cr.Mutation.AddStoryTag = func(childComplexity int, input graphcore.StoryTagAddInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.RemoveStoryTag = func(childComplexity int, input input.StoryTagRemoveInput) int {
|
||||
cr.Mutation.RemoveStoryTag = func(childComplexity int, input graphcore.StoryTagRemoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.EditStory = func(childComplexity int, input input.StoryEditInput) int {
|
||||
cr.Mutation.EditStory = func(childComplexity int, input graphcore.StoryEditInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.RemoveStory = func(childComplexity int, input input.StoryRemoveInput) int {
|
||||
cr.Mutation.RemoveStory = func(childComplexity int, input graphcore.StoryRemoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.AddChapter = func(childComplexity int, input input.ChapterAddInput) int {
|
||||
cr.Mutation.AddChapter = func(childComplexity int, input graphcore.ChapterAddInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.EditChapter = func(childComplexity int, input input.ChapterEditInput) int {
|
||||
cr.Mutation.EditChapter = func(childComplexity int, input graphcore.ChapterEditInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.MoveChapter = func(childComplexity int, input input.ChapterMoveInput) int {
|
||||
cr.Mutation.MoveChapter = func(childComplexity int, input graphcore.ChapterMoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.RemoveChapter = func(childComplexity int, input input.ChapterRemoveInput) int {
|
||||
cr.Mutation.RemoveChapter = func(childComplexity int, input graphcore.ChapterRemoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.AddComment = func(childComplexity int, input input.CommentAddInput) int {
|
||||
cr.Mutation.AddComment = func(childComplexity int, input graphcore.CommentAddInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.EditComment = func(childComplexity int, input input.CommentEditInput) int {
|
||||
cr.Mutation.EditComment = func(childComplexity int, input graphcore.CommentEditInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.RemoveComment = func(childComplexity int, input input.CommentRemoveInput) int {
|
||||
cr.Mutation.RemoveComment = func(childComplexity int, input graphcore.CommentRemoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.AddLog = func(childComplexity int, input input.LogAddInput) int {
|
||||
cr.Mutation.AddLog = func(childComplexity int, input graphcore.LogAddInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.ImportLog = func(childComplexity int, input input.LogImportInput) int {
|
||||
cr.Mutation.ImportLog = func(childComplexity int, input graphcore.LogImportInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.EditLog = func(childComplexity int, input input.LogEditInput) int {
|
||||
cr.Mutation.EditLog = func(childComplexity int, input graphcore.LogEditInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.RemoveLog = func(childComplexity int, input input.LogRemoveInput) int {
|
||||
cr.Mutation.RemoveLog = func(childComplexity int, input graphcore.LogRemoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.AddPost = func(childComplexity int, input input.PostAddInput) int {
|
||||
cr.Mutation.AddPost = func(childComplexity int, input graphcore.PostAddInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.EditPost = func(childComplexity int, input input.PostEditInput) int {
|
||||
cr.Mutation.EditPost = func(childComplexity int, input graphcore.PostEditInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.MovePost = func(childComplexity int, input input.PostMoveInput) int {
|
||||
cr.Mutation.MovePost = func(childComplexity int, input graphcore.PostMoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.RemovePost = func(childComplexity int, input input.PostRemoveInput) int {
|
||||
cr.Mutation.RemovePost = func(childComplexity int, input graphcore.PostRemoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.AddCharacter = func(childComplexity int, input input.CharacterAddInput) int {
|
||||
cr.Mutation.AddCharacter = func(childComplexity int, input graphcore.CharacterAddInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.AddCharacterNick = func(childComplexity int, input input.CharacterNickInput) int {
|
||||
cr.Mutation.AddCharacterNick = func(childComplexity int, input graphcore.CharacterNickInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.RemoveCharacterNick = func(childComplexity int, input input.CharacterNickInput) int {
|
||||
cr.Mutation.RemoveCharacterNick = func(childComplexity int, input graphcore.CharacterNickInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.EditCharacter = func(childComplexity int, input input.CharacterEditInput) int {
|
||||
cr.Mutation.EditCharacter = func(childComplexity int, input graphcore.CharacterEditInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.RemoveCharacter = func(childComplexity int, input input.CharacterRemoveInput) int {
|
||||
cr.Mutation.RemoveCharacter = func(childComplexity int, input graphcore.CharacterRemoveInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.AddChannel = func(childComplexity int, input input.ChannelAddInput) int {
|
||||
cr.Mutation.AddChannel = func(childComplexity int, input graphcore.ChannelAddInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
cr.Mutation.EditChannel = func(childComplexity int, input input.ChannelEditInput) int {
|
||||
cr.Mutation.EditChannel = func(childComplexity int, input graphcore.ChannelEditInput) int {
|
||||
return childComplexity + mutationComplexity
|
||||
}
|
||||
|
||||
@@ -166,6 +164,12 @@ func complexity() (cr ComplexityRoot) {
|
||||
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
|
||||
|
||||
+14
-23
@@ -3,12 +3,12 @@ schema:
|
||||
- schema/types/*.gql
|
||||
|
||||
exec:
|
||||
filename: generated.go
|
||||
package: graph2
|
||||
filename: graphcore/exec_gen.go
|
||||
package: graphcore
|
||||
|
||||
model:
|
||||
filename: input/generated.go
|
||||
package: input
|
||||
filename: graphcore/input_gen.go
|
||||
package: graphcore
|
||||
|
||||
models:
|
||||
Tag:
|
||||
@@ -20,48 +20,41 @@ models:
|
||||
Character:
|
||||
model: git.aiterp.net/rpdata/api/models.Character
|
||||
CharactersFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/characters.Filter
|
||||
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/channels.Filter
|
||||
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/posts.Filter
|
||||
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/logs.Filter
|
||||
model: git.aiterp.net/rpdata/api/models.LogFilter
|
||||
LogImporter:
|
||||
model: git.aiterp.net/rpdata/api/models.LogImporter
|
||||
LogSuggestion:
|
||||
model: git.aiterp.net/rpdata/api/models.LogSuggestion
|
||||
Comment:
|
||||
model: git.aiterp.net/rpdata/api/models.Comment
|
||||
fields:
|
||||
fictionalDate:
|
||||
resolver: true
|
||||
ChapterCommentMode:
|
||||
model: git.aiterp.net/rpdata/api/models.ChapterCommentMode
|
||||
Chapter:
|
||||
model: git.aiterp.net/rpdata/api/models.Chapter
|
||||
fields:
|
||||
fictionalDate:
|
||||
resolver: true
|
||||
Story:
|
||||
model: git.aiterp.net/rpdata/api/models.Story
|
||||
fields:
|
||||
fictionalDate:
|
||||
resolver: true
|
||||
StoryCategory:
|
||||
model: git.aiterp.net/rpdata/api/models.StoryCategory
|
||||
StoriesFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/stories.Filter
|
||||
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/files.Filter
|
||||
model: git.aiterp.net/rpdata/api/models.FileFilter
|
||||
Change:
|
||||
model: git.aiterp.net/rpdata/api/models.Change
|
||||
ChangeModel:
|
||||
@@ -71,10 +64,8 @@ models:
|
||||
ChangeKeyInput: # It's the same as ChangeKey
|
||||
model: git.aiterp.net/rpdata/api/models.ChangeKey
|
||||
ChangesFilter:
|
||||
model: git.aiterp.net/rpdata/api/models/changes.Filter
|
||||
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
|
||||
Date:
|
||||
model: git.aiterp.net/rpdata/api/models/scalars.Date
|
||||
model: git.aiterp.net/rpdata/api/models.User
|
||||
+27
-23
@@ -1,55 +1,59 @@
|
||||
package graph2
|
||||
|
||||
import (
|
||||
"git.aiterp.net/rpdata/api/graph2/queries"
|
||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||
"git.aiterp.net/rpdata/api/graph2/resolvers"
|
||||
"git.aiterp.net/rpdata/api/graph2/types"
|
||||
"git.aiterp.net/rpdata/api/services"
|
||||
graphql "github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/99designs/gqlgen -v
|
||||
|
||||
// New creates a new GraphQL schema.
|
||||
func New() graphql.ExecutableSchema {
|
||||
return NewExecutableSchema(Config{
|
||||
Resolvers: &rootResolver{},
|
||||
func New(s *services.Bundle) graphql.ExecutableSchema {
|
||||
return graphcore.NewExecutableSchema(graphcore.Config{
|
||||
Resolvers: &rootResolver{s},
|
||||
Complexity: complexity(),
|
||||
})
|
||||
}
|
||||
|
||||
type rootResolver struct{}
|
||||
|
||||
func (r *rootResolver) Query() QueryResolver {
|
||||
return &queries.Resolver
|
||||
type rootResolver struct {
|
||||
s *services.Bundle
|
||||
}
|
||||
|
||||
func (r *rootResolver) Mutation() MutationResolver {
|
||||
return queries.MutationResolver
|
||||
func (r *rootResolver) Query() graphcore.QueryResolver {
|
||||
return resolvers.QueryResolver(r.s)
|
||||
}
|
||||
|
||||
func (r *rootResolver) Subscription() SubscriptionResolver {
|
||||
return queries.SubscriptionResolver
|
||||
func (r *rootResolver) Mutation() graphcore.MutationResolver {
|
||||
return resolvers.MutationResolver(r.s)
|
||||
}
|
||||
|
||||
func (r *rootResolver) Log() LogResolver {
|
||||
return &types.LogResolver
|
||||
func (r *rootResolver) Subscription() graphcore.SubscriptionResolver {
|
||||
return resolvers.SubscriptionResolver(r.s)
|
||||
}
|
||||
|
||||
func (r *rootResolver) Comment() CommentResolver {
|
||||
return &types.CommentResolver
|
||||
func (r *rootResolver) Log() graphcore.LogResolver {
|
||||
return types.LogResolver(r.s)
|
||||
}
|
||||
|
||||
func (r *rootResolver) Chapter() ChapterResolver {
|
||||
return &types.ChapterResolver
|
||||
func (r *rootResolver) Comment() graphcore.CommentResolver {
|
||||
return types.CommentResolver(r.s)
|
||||
}
|
||||
|
||||
func (r *rootResolver) Story() StoryResolver {
|
||||
return &types.StoryResolver
|
||||
func (r *rootResolver) Chapter() graphcore.ChapterResolver {
|
||||
return types.ChapterResolver(r.s)
|
||||
}
|
||||
|
||||
func (r *rootResolver) Change() ChangeResolver {
|
||||
func (r *rootResolver) Story() graphcore.StoryResolver {
|
||||
return types.StoryResolver(r.s)
|
||||
}
|
||||
|
||||
func (r *rootResolver) Change() graphcore.ChangeResolver {
|
||||
return &types.ChangeResolver
|
||||
}
|
||||
|
||||
func (r *rootResolver) Token() TokenResolver {
|
||||
return &types.TokenResolver
|
||||
func (r *rootResolver) Token() graphcore.TokenResolver {
|
||||
return types.TokenResolver(r.s)
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
)
|
||||
|
||||
/// Queries
|
||||
|
||||
func (r *resolver) Changes(ctx context.Context, filter *changes.Filter) ([]*models.Change, error) {
|
||||
changes, err := changes.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changes2 := make([]*models.Change, len(changes))
|
||||
for i := range changes {
|
||||
changes2[i] = &changes[i]
|
||||
}
|
||||
|
||||
return changes2, nil
|
||||
}
|
||||
|
||||
/// Subscriptions
|
||||
|
||||
func (r *subscriptionResolver) Changes(ctx context.Context, keys []*models.ChangeKey) (<-chan *models.Change, error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, errors.New("At least one key is required for a subscription")
|
||||
}
|
||||
|
||||
keys2 := make([]models.ChangeKey, len(keys))
|
||||
for i := range keys {
|
||||
keys2[i] = *keys[i]
|
||||
}
|
||||
|
||||
return changes.Subscribe(ctx, keys2, false), nil
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/channels"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Channel(ctx context.Context, name string) (*models.Channel, error) {
|
||||
channel, err := channels.FindName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *resolver) Channels(ctx context.Context, filter *channels.Filter) ([]*models.Channel, error) {
|
||||
channels, err := channels.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels2 := make([]*models.Channel, len(channels))
|
||||
for i := range channels {
|
||||
channels2[i] = &channels[i]
|
||||
}
|
||||
|
||||
return channels2, nil
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddChannel(ctx context.Context, input input.ChannelAddInput) (*models.Channel, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("channel.add") {
|
||||
return nil, errors.New("You are not permitted to add channels")
|
||||
}
|
||||
|
||||
logged := false
|
||||
if input.Logged != nil {
|
||||
logged = *input.Logged
|
||||
}
|
||||
hub := false
|
||||
if input.Hub != nil {
|
||||
hub = *input.Hub
|
||||
}
|
||||
eventName := ""
|
||||
if input.EventName != nil {
|
||||
eventName = *input.EventName
|
||||
}
|
||||
locationName := ""
|
||||
if input.LocationName != nil {
|
||||
locationName = *input.LocationName
|
||||
}
|
||||
|
||||
channel, err := channels.Add(input.Name, logged, hub, eventName, locationName)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to add channel: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Channel", "add", token.UserID, true, changekeys.Listed(channel), channel)
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditChannel(ctx context.Context, input input.ChannelEditInput) (*models.Channel, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("channel.edit") {
|
||||
return nil, errors.New("You are not permitted to edit channels")
|
||||
}
|
||||
|
||||
channel, err := channels.FindName(input.Name)
|
||||
if err != nil {
|
||||
return nil, errors.New("Channel not found")
|
||||
}
|
||||
|
||||
channel, err = channels.Edit(channel, input.Logged, input.Hub, input.EventName, input.LocationName)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to edit channel: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Channel", "edit", token.UserID, true, changekeys.Listed(channel), channel)
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
"git.aiterp.net/rpdata/api/models/comments"
|
||||
"git.aiterp.net/rpdata/api/models/stories"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Chapter(ctx context.Context, id string) (*models.Chapter, error) {
|
||||
chapter, err := chapters.FindID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddChapter(ctx context.Context, input input.ChapterAddInput) (*models.Chapter, error) {
|
||||
story, err := stories.FindID(input.StoryID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Story not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "story.add") {
|
||||
return nil, errors.New("Unauthorized")
|
||||
}
|
||||
|
||||
author := token.UserID
|
||||
if input.Author != nil && *input.Author != author {
|
||||
if !token.Permitted("story.add") {
|
||||
return nil, errors.New("False pretender")
|
||||
}
|
||||
|
||||
author = *input.Author
|
||||
}
|
||||
|
||||
if !story.Open && story.Author != author {
|
||||
return nil, errors.New("Story is not open")
|
||||
}
|
||||
|
||||
commentMode := models.ChapterCommentModeDisabled
|
||||
if input.CommentMode != nil {
|
||||
commentMode = *input.CommentMode
|
||||
}
|
||||
|
||||
chapter, err := chapters.Add(story, input.Title, author, input.Source, time.Now(), input.FictionalDate, commentMode)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to create chapter: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Chapter", "add", token.UserID, story.Listed, changekeys.Listed(story, chapter), story, chapter)
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MoveChapter(ctx context.Context, input input.ChapterMoveInput) (*models.Chapter, error) {
|
||||
chapter, err := chapters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Chapter not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "chapter.move") {
|
||||
return nil, errors.New("You are not allowed to move this chapter")
|
||||
}
|
||||
|
||||
target, err := stories.FindID(input.StoryID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Target story not found")
|
||||
}
|
||||
|
||||
if !target.Open && !token.PermittedUser(target.Author, "member", "chapter.move") {
|
||||
return nil, errors.New("You are not permitted to move chapters to this story")
|
||||
}
|
||||
|
||||
oldStoryID := chapter.StoryID
|
||||
chapter, err = chapters.Move(chapter, target)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to move chapter: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
story, err := stories.FindID(chapter.StoryID)
|
||||
if err != nil {
|
||||
story.ID = chapter.StoryID
|
||||
}
|
||||
|
||||
oldStory, err := stories.FindID(oldStoryID)
|
||||
if err != nil {
|
||||
oldStory.ID = oldStoryID
|
||||
}
|
||||
|
||||
changes.Submit("Chapter", "move-out", chapter.Author, oldStory.Listed, changekeys.Many(oldStory, chapter), chapter)
|
||||
changes.Submit("Chapter", "move-in", token.UserID, story.Listed, changekeys.Listed(story, chapter), story, chapter)
|
||||
}()
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditChapter(ctx context.Context, input input.ChapterEditInput) (*models.Chapter, error) {
|
||||
chapter, err := chapters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Chapter not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "chapter.edit") {
|
||||
return nil, errors.New("Unauthorized")
|
||||
}
|
||||
|
||||
if input.ClearFictionalDate != nil && *input.ClearFictionalDate == true {
|
||||
input.FictionalDate = &time.Time{}
|
||||
}
|
||||
|
||||
chapter, err = chapters.Edit(chapter, input.Title, input.Source, input.FictionalDate, input.CommentMode, input.CommentsLocked)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to edit chapter: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
story, err := stories.FindID(chapter.StoryID)
|
||||
if err != nil {
|
||||
story.ID = chapter.StoryID
|
||||
}
|
||||
|
||||
changes.Submit("Chapter", "edit", token.UserID, story.Listed, changekeys.Many(story, chapter), chapter)
|
||||
}()
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveChapter(ctx context.Context, input input.ChapterRemoveInput) (*models.Chapter, error) {
|
||||
chapter, err := chapters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Chapter not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.PermittedUser(chapter.Author, "member", "chapter.remove") {
|
||||
return nil, errors.New("Unauthorized")
|
||||
}
|
||||
|
||||
chapter, err = chapters.Remove(chapter)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to remove chapter: " + err.Error())
|
||||
}
|
||||
|
||||
err = comments.RemoveChapter(chapter)
|
||||
if err != nil {
|
||||
return nil, errors.New("Chapter was removed, but comment removal failed: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
story, err := stories.FindID(chapter.StoryID)
|
||||
if err != nil {
|
||||
story.ID = chapter.StoryID
|
||||
}
|
||||
|
||||
changes.Submit("Chapter", "remove", token.UserID, story.Listed, changekeys.Many(story, chapter), chapter)
|
||||
}()
|
||||
|
||||
return &chapter, nil
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/characters"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Character(ctx context.Context, id *string, nick *string) (*models.Character, error) {
|
||||
if id != nil {
|
||||
character, err := characters.FindID(*id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &character, nil
|
||||
} else if nick != nil {
|
||||
character, err := characters.FindNick(*nick)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &character, nil
|
||||
} else {
|
||||
return nil, errors.New("You must specify either an ID or a nick")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *resolver) Characters(ctx context.Context, filter *characters.Filter) ([]*models.Character, error) {
|
||||
characters, err := characters.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
characters2 := make([]*models.Character, len(characters))
|
||||
for i := range characters {
|
||||
characters2[i] = &characters[i]
|
||||
}
|
||||
|
||||
return characters2, nil
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddCharacter(ctx context.Context, input input.CharacterAddInput) (*models.Character, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "character.add") {
|
||||
return nil, errors.New("You are not permitted to add characters")
|
||||
}
|
||||
if len(input.Name) < 2 || len(input.Nick) < 2 {
|
||||
return nil, errors.New("You need to provide a name and a nick (min length: 2)")
|
||||
}
|
||||
|
||||
shortName := ""
|
||||
if input.ShortName != nil {
|
||||
shortName = *input.ShortName
|
||||
} else {
|
||||
shortName = strings.SplitN(input.Name, " ", 2)[0]
|
||||
}
|
||||
|
||||
description := ""
|
||||
if input.Description != nil {
|
||||
description = *input.Description
|
||||
}
|
||||
|
||||
author := token.UserID
|
||||
if input.Author != nil && *input.Author != author {
|
||||
if !token.Permitted("character.add") {
|
||||
return nil, errors.New("You are only permitted to add your own characters")
|
||||
}
|
||||
|
||||
author = *input.Author
|
||||
}
|
||||
|
||||
logs.ScheduleFullUpdate()
|
||||
|
||||
character, err := characters.Add(input.Nick, input.Name, shortName, author, description)
|
||||
if err != nil {
|
||||
return nil, errors.New("Adding character failed: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Character", "add", token.UserID, true, changekeys.Listed(character), character)
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddCharacterNick(ctx context.Context, input input.CharacterNickInput) (*models.Character, error) {
|
||||
character, err := characters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Character not found")
|
||||
}
|
||||
|
||||
if len(input.Nick) < 2 {
|
||||
return nil, errors.New("You need to provide a valid nick (min length: 2)")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(character.Author, "member", "character.edit") {
|
||||
return nil, errors.New("You are not permitted to edit this character")
|
||||
}
|
||||
|
||||
logs.ScheduleFullUpdate()
|
||||
|
||||
character, err = characters.AddNick(character, input.Nick)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to add nick: " + err.Error())
|
||||
}
|
||||
|
||||
go logs.ScheduleFullUpdate()
|
||||
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveCharacterNick(ctx context.Context, input input.CharacterNickInput) (*models.Character, error) {
|
||||
character, err := characters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Character not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(character.Author, "member", "character.edit") {
|
||||
return nil, errors.New("You are not permitted to edit this character")
|
||||
}
|
||||
|
||||
character, err = characters.RemoveNick(character, input.Nick)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to remove nick: " + err.Error())
|
||||
}
|
||||
|
||||
go logs.ScheduleFullUpdate()
|
||||
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditCharacter(ctx context.Context, input input.CharacterEditInput) (*models.Character, error) {
|
||||
character, err := characters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Character not found")
|
||||
}
|
||||
|
||||
if input.Name != nil && len(*input.Name) < 2 {
|
||||
return nil, errors.New("You need to provide a valid name (min length: 2)")
|
||||
}
|
||||
if input.ShortName != nil && len(*input.ShortName) < 2 {
|
||||
return nil, errors.New("You need to provide a valid short name (min length: 2)")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(character.Author, "member", "character.edit") {
|
||||
return nil, errors.New("You are not permitted to edit this character")
|
||||
}
|
||||
|
||||
character, err = characters.Edit(character, input.Name, input.ShortName, input.Description)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to edit character: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveCharacter(ctx context.Context, input input.CharacterRemoveInput) (*models.Character, error) {
|
||||
character, err := characters.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Character not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(character.Author, "member", "character.remove") {
|
||||
return nil, errors.New("You are not permitted to remove this character")
|
||||
}
|
||||
|
||||
character, err = characters.Remove(character)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to remove character: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Character", "remove", token.UserID, true, changekeys.Listed(character), character)
|
||||
|
||||
return &character, nil
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/stories"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/characters"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
"git.aiterp.net/rpdata/api/models/comments"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Comment(ctx context.Context, id string) (*models.Comment, error) {
|
||||
comment, err := comments.Find(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddComment(ctx context.Context, input input.CommentAddInput) (*models.Comment, error) {
|
||||
chapter, err := chapters.FindID(input.ChapterID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Chapter not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Permitted("member", "story.edit") {
|
||||
return nil, errors.New("Unauthorized")
|
||||
}
|
||||
|
||||
if !chapter.CanComment() {
|
||||
return nil, errors.New("Comments are disabled or locked")
|
||||
}
|
||||
|
||||
var characterPtr *models.Character
|
||||
if input.CharacterID != nil {
|
||||
character, err := characters.FindID(*input.CharacterID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Character not found")
|
||||
} else if character.Author != token.UserID {
|
||||
return nil, errors.New("That is not your character")
|
||||
}
|
||||
|
||||
characterPtr = &character
|
||||
}
|
||||
|
||||
fictionalDate := time.Time{}
|
||||
if input.FictionalDate != nil {
|
||||
fictionalDate = *input.FictionalDate
|
||||
}
|
||||
|
||||
subject := ""
|
||||
if input.Subject != nil {
|
||||
subject = *input.Subject
|
||||
}
|
||||
|
||||
comment, err := comments.Add(chapter, subject, token.UserID, input.Source, input.CharacterName, characterPtr, time.Now(), fictionalDate)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to add comment: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
story, err := stories.FindID(chapter.StoryID)
|
||||
if err != nil {
|
||||
log.Println("WARNING: Couldn't log comment change:", err)
|
||||
return
|
||||
}
|
||||
|
||||
changes.Submit("Comment", "add", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story)
|
||||
}()
|
||||
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditComment(ctx context.Context, input input.CommentEditInput) (*models.Comment, error) {
|
||||
comment, err := comments.Find(input.CommentID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Comment not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(comment.Author, "member", "story.edit") {
|
||||
return nil, errors.New("You cannot edit this comment")
|
||||
}
|
||||
|
||||
chapter, err := chapters.FindID(comment.ChapterID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Comment's chapter not found")
|
||||
}
|
||||
|
||||
if !chapter.CanComment() {
|
||||
return nil, errors.New("Comments are disabled or locked")
|
||||
}
|
||||
|
||||
if input.ClearFictionalDate != nil && *input.ClearFictionalDate == true {
|
||||
input.FictionalDate = &time.Time{}
|
||||
}
|
||||
|
||||
if input.CharacterID != nil && *input.CharacterID != "" {
|
||||
character, err := characters.FindID(*input.CharacterID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Character not found")
|
||||
} else if character.Author != token.UserID {
|
||||
return nil, errors.New("That is not your character")
|
||||
}
|
||||
}
|
||||
|
||||
comment, err = comments.Edit(comment, input.Source, input.CharacterName, input.CharacterID, input.Subject, input.FictionalDate)
|
||||
if err != nil {
|
||||
return nil, errors.New("Could not post comment: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
story, err := stories.FindID(chapter.StoryID)
|
||||
if err != nil {
|
||||
log.Println("WARNING: Couldn't log comment change:", err)
|
||||
return
|
||||
}
|
||||
|
||||
changes.Submit("Comment", "edit", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story)
|
||||
}()
|
||||
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveComment(ctx context.Context, input input.CommentRemoveInput) (*models.Comment, error) {
|
||||
comment, err := comments.Find(input.CommentID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Comment not found")
|
||||
}
|
||||
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.PermittedUser(comment.Author, "member", "story.edit") {
|
||||
return nil, errors.New("You cannot remove this comment")
|
||||
}
|
||||
|
||||
chapter, err := chapters.FindID(comment.ChapterID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Comment's chapter not found")
|
||||
}
|
||||
|
||||
if !chapter.CanComment() {
|
||||
return nil, errors.New("Comments are disabled or locked")
|
||||
}
|
||||
|
||||
err = comments.Remove(comment)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to remove comment: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
story, err := stories.FindID(chapter.StoryID)
|
||||
if err != nil {
|
||||
log.Println("WARNING: Couldn't log comment change:", err)
|
||||
return
|
||||
}
|
||||
|
||||
changes.Submit("Comment", "remove", token.UserID, true, changekeys.Many(comment, chapter, models.Story{ID: chapter.StoryID}), comment, chapter, story)
|
||||
}()
|
||||
|
||||
return &comment, nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/files"
|
||||
)
|
||||
|
||||
func (r *resolver) File(ctx context.Context, id string) (*models.File, error) {
|
||||
file, err := files.FindID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
func (r *resolver) Files(ctx context.Context, filter *files.Filter) ([]*models.File, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
if filter == nil {
|
||||
filter = &files.Filter{}
|
||||
}
|
||||
|
||||
// Only allow users to view public files that are not their own.
|
||||
if token != nil {
|
||||
if filter.Public == nil || *filter.Public == false {
|
||||
filter.Author = &token.UserID
|
||||
}
|
||||
} else {
|
||||
filter.Public = &trueValue
|
||||
}
|
||||
|
||||
files, err := files.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files2 := make([]*models.File, len(files))
|
||||
for i := range files {
|
||||
files2[i] = &files[i]
|
||||
}
|
||||
|
||||
return files2, nil
|
||||
}
|
||||
|
||||
var trueValue = true
|
||||
@@ -1,194 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/channels"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/internal/loader"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Log(ctx context.Context, id string) (*models.Log, error) {
|
||||
log, err := logs.FindID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &log, nil
|
||||
}
|
||||
|
||||
func (r *resolver) Logs(ctx context.Context, filter *logs.Filter) ([]*models.Log, error) {
|
||||
logs, err := logs.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqCtx := graphql.GetRequestContext(ctx)
|
||||
maybeCharacters := strings.Contains(reqCtx.RawQuery, "characters")
|
||||
maybeChannels := strings.Contains(reqCtx.RawQuery, "channels")
|
||||
|
||||
if len(logs) >= 100 && (maybeCharacters || maybeChannels) {
|
||||
loader := loader.FromContext(ctx)
|
||||
if loader == nil {
|
||||
return nil, errors.New("no loader")
|
||||
}
|
||||
|
||||
for _, log := range logs {
|
||||
if maybeChannels {
|
||||
loader.PrimeChannels("name", log.ChannelName)
|
||||
}
|
||||
|
||||
if maybeCharacters {
|
||||
loader.PrimeCharacters("id", log.CharacterIDs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logs2 := make([]*models.Log, len(logs))
|
||||
for i := range logs {
|
||||
logs2[i] = &logs[i]
|
||||
}
|
||||
|
||||
return logs2, nil
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddLog(ctx context.Context, input input.LogAddInput) (*models.Log, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("log.add") {
|
||||
return nil, errors.New("You are not permitted to add logs")
|
||||
}
|
||||
|
||||
open := input.Open != nil && *input.Open == true
|
||||
title := ""
|
||||
if input.Title != nil {
|
||||
title = *input.Title
|
||||
}
|
||||
event := ""
|
||||
if input.Event != nil {
|
||||
event = *input.Event
|
||||
}
|
||||
description := ""
|
||||
if input.Description != nil {
|
||||
description = *input.Description
|
||||
}
|
||||
|
||||
existingChannel, _ := channels.FindName(input.Channel)
|
||||
|
||||
log, err := logs.Add(input.Date, input.Channel, title, event, description, open)
|
||||
if !token.Authenticated() || !token.Permitted("log.add") {
|
||||
return nil, errors.New("Failed to create log: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
if existingChannel.Name == "" {
|
||||
channel, err := channels.FindName(input.Channel)
|
||||
if err == nil {
|
||||
changes.Submit("Channel", "add", token.UserID, true, changekeys.Listed(channel), channel)
|
||||
}
|
||||
}
|
||||
|
||||
changes.Submit("Log", "add", token.UserID, true, changekeys.Listed(log), log)
|
||||
}()
|
||||
|
||||
return &log, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImportLog(ctx context.Context, input input.LogImportInput) ([]*models.Log, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("log.add") {
|
||||
return nil, errors.New("You are not permitted to add logs")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
results, err := logs.Import(input.Importer, date, tz, input.ChannelName, input.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newLogs := make([]*models.Log, 0, len(results))
|
||||
for _, result := range results {
|
||||
go func(result logs.ImportedLog) {
|
||||
changes.Submit("Log", "add", token.UserID, true, changekeys.Many(result.Log), result.Log)
|
||||
changes.Submit("Post", "add", token.UserID, true, changekeys.Many(result.Log, result.Posts), result.Posts)
|
||||
}(result)
|
||||
|
||||
log, err := logs.UpdateCharacters(result.Log, nil)
|
||||
if err != nil {
|
||||
log = result.Log
|
||||
}
|
||||
|
||||
newLogs = append(newLogs, &log)
|
||||
}
|
||||
|
||||
return newLogs, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditLog(ctx context.Context, input input.LogEditInput) (*models.Log, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("log.edit") {
|
||||
return nil, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
log, err := logs.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Log not found")
|
||||
}
|
||||
|
||||
log, err = logs.Edit(log, input.Title, input.Event, input.Description, input.Open)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to edit log: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Log", "edit", token.UserID, true, changekeys.Listed(log), log)
|
||||
|
||||
return &log, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveLog(ctx context.Context, input input.LogRemoveInput) (*models.Log, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("log.remove") {
|
||||
return nil, errors.New("You are not permitted to remove logs")
|
||||
}
|
||||
|
||||
log, err := logs.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Log not found")
|
||||
}
|
||||
|
||||
log, err = logs.Remove(log)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to remove log: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Log", "remove", token.UserID, true, changekeys.Listed(log), log)
|
||||
|
||||
return &log, nil
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
"git.aiterp.net/rpdata/api/models/logs"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/posts"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *resolver) Post(ctx context.Context, id string) (*models.Post, error) {
|
||||
post, err := posts.FindID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (r *resolver) Posts(ctx context.Context, filter *posts.Filter) ([]*models.Post, error) {
|
||||
// Some sanity checks to avoid querying an insame amount of posts.
|
||||
if filter == nil {
|
||||
filter = &posts.Filter{Limit: 256}
|
||||
} else {
|
||||
if (filter.Limit <= 0 || filter.Limit > 256) && filter.LogID == nil {
|
||||
return nil, errors.New("a limit of 0 (no limit) or >256 without a logId is not allowed")
|
||||
}
|
||||
|
||||
if len(filter.Kind) > 32 {
|
||||
return nil, errors.New("You cannot specify more than 32 kinds")
|
||||
}
|
||||
|
||||
if len(filter.ID) > 32 {
|
||||
return nil, errors.New("You cannot specify more than 32 IDs")
|
||||
}
|
||||
}
|
||||
|
||||
posts, err := posts.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
posts2 := make([]*models.Post, len(posts))
|
||||
for i := range posts {
|
||||
posts2[i] = &posts[i]
|
||||
}
|
||||
|
||||
return posts2, nil
|
||||
}
|
||||
|
||||
// Mutation
|
||||
|
||||
func (r *mutationResolver) AddPost(ctx context.Context, input input.PostAddInput) (*models.Post, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("post.add") {
|
||||
return nil, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
log, err := logs.FindID(input.LogID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
post, err := posts.Add(log, input.Time, input.Kind, input.Nick, input.Text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go logs.UpdateCharacters(log, nil)
|
||||
go changes.Submit("Post", "add", token.UserID, true, changekeys.Many(log, post), post)
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditPost(ctx context.Context, input input.PostEditInput) (*models.Post, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("post.edit") {
|
||||
return nil, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
post, err := posts.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Post not found")
|
||||
}
|
||||
|
||||
if input.Nick != nil {
|
||||
go func() {
|
||||
log, err := logs.FindID(post.LogID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logs.UpdateCharacters(log, nil)
|
||||
}()
|
||||
}
|
||||
|
||||
post, err = posts.Edit(post, input.Time, input.Kind, input.Nick, input.Text)
|
||||
if err != nil {
|
||||
return nil, errors.New("Adding post failed: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
log, err := logs.FindID(post.LogID)
|
||||
if err != nil {
|
||||
log = models.Log{ID: post.LogID}
|
||||
}
|
||||
|
||||
changes.Submit("Post", "edit", token.UserID, true, changekeys.Many(log, post), post)
|
||||
}()
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovePost(ctx context.Context, input input.PostMoveInput) ([]*models.Post, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("post.move") {
|
||||
return nil, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
post, err := posts.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Post not found")
|
||||
}
|
||||
|
||||
posts, err := posts.Move(post, input.ToPosition)
|
||||
if err != nil {
|
||||
return nil, errors.New("Moving posts failed: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
log, err := logs.FindID(post.LogID)
|
||||
if err != nil {
|
||||
log = models.Log{ID: post.LogID}
|
||||
}
|
||||
|
||||
changes.Submit("Post", "move", token.UserID, true, changekeys.Many(log, posts), posts)
|
||||
}()
|
||||
|
||||
posts2 := make([]*models.Post, len(posts))
|
||||
for i := range posts {
|
||||
posts2[i] = &posts[i]
|
||||
}
|
||||
|
||||
return posts2, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemovePost(ctx context.Context, input input.PostRemoveInput) (*models.Post, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() || !token.Permitted("post.remove") {
|
||||
return nil, errors.New("You are not permitted to edit logs")
|
||||
}
|
||||
|
||||
post, err := posts.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Post not found (before removing, of course)")
|
||||
}
|
||||
|
||||
post, err = posts.Remove(post)
|
||||
if err != nil {
|
||||
return nil, errors.New("Could not remove post: " + err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
log, err := logs.FindID(post.LogID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logs.UpdateCharacters(log, nil)
|
||||
changes.Submit("Post", "remove", token.UserID, true, changekeys.Many(log, post), post)
|
||||
}()
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package queries
|
||||
|
||||
type resolver struct{}
|
||||
type mutationResolver struct{}
|
||||
type subscriptionResolver struct{}
|
||||
|
||||
// Resolver has all the queries
|
||||
var Resolver resolver
|
||||
|
||||
// MutationResolver brings the mutagens.
|
||||
var MutationResolver *mutationResolver
|
||||
|
||||
// SubscriptionResolver has the real-time magic.
|
||||
var SubscriptionResolver *subscriptionResolver
|
||||
@@ -1,201 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
||||
"git.aiterp.net/rpdata/api/models/changes"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
"git.aiterp.net/rpdata/api/models/stories"
|
||||
)
|
||||
|
||||
func (r *resolver) Story(ctx context.Context, id string) (*models.Story, error) {
|
||||
story, err := stories.FindID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &story, nil
|
||||
}
|
||||
|
||||
func (r *resolver) Stories(ctx context.Context, filter *stories.Filter) ([]*models.Story, error) {
|
||||
if filter != nil {
|
||||
if filter.Unlisted != nil && *filter.Unlisted == true {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() {
|
||||
return nil, errors.New("You are not permitted to view unlisted stories")
|
||||
}
|
||||
|
||||
if !token.Permitted("story.unlisted") {
|
||||
filter.Author = &token.UserID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stories, err := stories.List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stories2 := make([]*models.Story, len(stories))
|
||||
for i := range stories {
|
||||
stories2[i] = &stories[i]
|
||||
}
|
||||
|
||||
return stories2, nil
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddStory(ctx context.Context, input input.StoryAddInput) (*models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if token == nil || !token.Permitted("member", "story.add") {
|
||||
return nil, errors.New("Permission denied")
|
||||
}
|
||||
|
||||
author := token.UserID
|
||||
if input.Author != nil && *input.Author != author {
|
||||
if !token.Permitted("story.add") {
|
||||
return nil, errors.New("You are not permitted to add a story in another author's name")
|
||||
}
|
||||
|
||||
author = *input.Author
|
||||
}
|
||||
|
||||
fictionalDate := time.Time{}
|
||||
if input.FictionalDate != nil {
|
||||
fictionalDate = *input.FictionalDate
|
||||
}
|
||||
|
||||
listed := input.Listed != nil && *input.Listed
|
||||
open := input.Open != nil && *input.Open
|
||||
|
||||
tags := make([]models.Tag, len(input.Tags))
|
||||
for i := range input.Tags {
|
||||
tags[i] = *input.Tags[i]
|
||||
}
|
||||
|
||||
story, err := stories.Add(input.Name, author, input.Category, listed, open, tags, time.Now(), fictionalDate)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to add story: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Story", "add", token.UserID, story.Listed, changekeys.Listed(story), story)
|
||||
|
||||
return &story, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddStoryTag(ctx context.Context, input input.StoryTagAddInput) (*models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
story, err := stories.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Story not found")
|
||||
}
|
||||
|
||||
if story.Open {
|
||||
if !token.Permitted("member") {
|
||||
return nil, errors.New("You are not permitted to edit this story")
|
||||
}
|
||||
} else {
|
||||
if !token.PermittedUser(story.Author, "member", "story.edit") {
|
||||
return nil, errors.New("You are not permitted to edit this story")
|
||||
}
|
||||
}
|
||||
|
||||
story, err = stories.AddTag(story, *input.Tag)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to add story: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Story", "tag", token.UserID, story.Listed, changekeys.Listed(story), story, input.Tag)
|
||||
|
||||
return &story, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveStoryTag(ctx context.Context, input input.StoryTagRemoveInput) (*models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
story, err := stories.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Story not found")
|
||||
}
|
||||
|
||||
if story.Open {
|
||||
if !token.Permitted("member") {
|
||||
return nil, errors.New("You are not permitted to edit this story")
|
||||
}
|
||||
} else {
|
||||
if !token.PermittedUser(story.Author, "member", "story.edit") {
|
||||
return nil, errors.New("You are not permitted to edit this story")
|
||||
}
|
||||
}
|
||||
|
||||
story, err = stories.RemoveTag(story, *input.Tag)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to add story: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Story", "untag", token.UserID, story.Listed, changekeys.Listed(story), story, input.Tag)
|
||||
|
||||
return &story, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditStory(ctx context.Context, input input.StoryEditInput) (*models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
story, err := stories.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Story not found")
|
||||
}
|
||||
|
||||
if !token.PermittedUser(story.Author, "member", "story.edit") {
|
||||
return nil, errors.New("You are not permitted to remove this story")
|
||||
}
|
||||
|
||||
if input.ClearFictionalDate != nil && *input.ClearFictionalDate {
|
||||
input.FictionalDate = &time.Time{}
|
||||
}
|
||||
|
||||
story, err = stories.Edit(story, input.Name, input.Category, input.Listed, input.Open, input.FictionalDate)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to add story: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Story", "edit", token.UserID, story.Listed, changekeys.Listed(story), story)
|
||||
|
||||
return &story, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveStory(ctx context.Context, input input.StoryRemoveInput) (*models.Story, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
|
||||
story, err := stories.FindID(input.ID)
|
||||
if err != nil {
|
||||
return nil, errors.New("Story not found")
|
||||
}
|
||||
|
||||
if !token.PermittedUser(story.Author, "member", "story.remove") {
|
||||
return nil, errors.New("You are not permitted to remove this story")
|
||||
}
|
||||
|
||||
story, err = stories.Remove(story)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = chapters.RemoveStory(story)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to remove chapters, but story is removed: " + err.Error())
|
||||
}
|
||||
|
||||
go changes.Submit("Story", "remove", token.UserID, story.Listed, changekeys.Listed(story), story)
|
||||
|
||||
return &story, nil
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/tags"
|
||||
)
|
||||
|
||||
func (r *resolver) Tags(ctx context.Context) ([]*models.Tag, error) {
|
||||
tags, err := tags.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags2 := make([]*models.Tag, len(tags))
|
||||
for i := range tags {
|
||||
tags2[i] = &tags[i]
|
||||
}
|
||||
|
||||
return tags2, nil
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/auth"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
func (r *resolver) Token(ctx context.Context) (*models.Token, error) {
|
||||
token := auth.TokenFromContext(ctx)
|
||||
if !token.Authenticated() {
|
||||
return nil, errors.New("No (valid) token")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/unknownnicks"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
)
|
||||
|
||||
/// Queries
|
||||
|
||||
func (r *resolver) UnknownNicks(ctx context.Context, filter *input.UnknownNicksFilter) ([]*models.UnknownNick, error) {
|
||||
limit := 100
|
||||
if filter != nil {
|
||||
if filter.Limit > 0 {
|
||||
limit = filter.Limit
|
||||
}
|
||||
}
|
||||
|
||||
nicks, err := unknownnicks.List(limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nicks2 := make([]*models.UnknownNick, len(nicks))
|
||||
for i := range nicks {
|
||||
nicks2[i] = &nicks[i]
|
||||
}
|
||||
|
||||
return nicks2, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
/// Queries
|
||||
|
||||
func (r *queryResolver) Changes(ctx context.Context, filter *models.ChangeFilter) ([]*models.Change, error) {
|
||||
if filter == nil {
|
||||
filter = &models.ChangeFilter{}
|
||||
}
|
||||
|
||||
return r.s.Changes.List(ctx, *filter)
|
||||
}
|
||||
|
||||
/// Subscriptions
|
||||
|
||||
func (r *subscriptionResolver) Changes(ctx context.Context, filter *models.ChangeFilter) (<-chan *models.Change, error) {
|
||||
if filter == nil {
|
||||
filter = &models.ChangeFilter{}
|
||||
}
|
||||
|
||||
return r.s.Changes.Subscribe(ctx, *filter), nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *queryResolver) Channel(ctx context.Context, name string) (*models.Channel, error) {
|
||||
return r.s.Channels.Find(ctx, name)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Channels(ctx context.Context, filter *models.ChannelFilter) ([]*models.Channel, error) {
|
||||
if filter == nil {
|
||||
filter = &models.ChannelFilter{}
|
||||
}
|
||||
|
||||
return r.s.Channels.List(ctx, *filter)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddChannel(ctx context.Context, input graphcore.ChannelAddInput) (*models.Channel, error) {
|
||||
logged := input.Logged != nil && *input.Logged
|
||||
hub := input.Hub != nil && *input.Hub
|
||||
|
||||
locationName := ""
|
||||
if input.LocationName != nil {
|
||||
locationName = *input.LocationName
|
||||
}
|
||||
|
||||
eventName := ""
|
||||
if input.LocationName != nil {
|
||||
eventName = *input.EventName
|
||||
}
|
||||
|
||||
return r.s.Channels.Create(ctx, input.Name, logged, hub, eventName, locationName)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditChannel(ctx context.Context, input graphcore.ChannelEditInput) (*models.Channel, error) {
|
||||
return r.s.Channels.Update(ctx, input.Name, models.ChannelUpdate{
|
||||
Logged: input.Logged,
|
||||
Hub: input.Hub,
|
||||
EventName: input.EventName,
|
||||
LocationName: input.LocationName,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *queryResolver) Chapter(ctx context.Context, id string) (*models.Chapter, error) {
|
||||
return r.s.Stories.FindChapter(ctx, id)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddChapter(ctx context.Context, input graphcore.ChapterAddInput) (*models.Chapter, error) {
|
||||
story, err := r.s.Stories.FindStory(ctx, input.StoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commentMode := models.ChapterCommentModeDisabled
|
||||
if input.CommentMode != nil {
|
||||
commentMode = *input.CommentMode
|
||||
}
|
||||
|
||||
return r.s.Stories.CreateChapter(ctx, *story, input.Title, input.Source, input.Author, time.Now(), input.FictionalDate, commentMode)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MoveChapter(ctx context.Context, input graphcore.ChapterMoveInput) (*models.Chapter, error) {
|
||||
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
from, err := r.s.Stories.FindStory(ctx, chapter.StoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
to, err := r.s.Stories.FindStory(ctx, input.StoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.s.Stories.MoveChapter(ctx, chapter, *from, *to)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditChapter(ctx context.Context, input graphcore.ChapterEditInput) (*models.Chapter, error) {
|
||||
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.s.Stories.EditChapter(ctx, chapter, input.Title, input.Source, input.FictionalDate, input.CommentMode, input.CommentsLocked)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveChapter(ctx context.Context, input graphcore.ChapterRemoveInput) (*models.Chapter, error) {
|
||||
chapter, err := r.s.Stories.FindChapter(ctx, input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.s.Stories.RemoveChapter(ctx, chapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chapter, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *queryResolver) Character(ctx context.Context, id *string, nick *string) (*models.Character, error) {
|
||||
if id != nil {
|
||||
return r.s.Characters.Find(ctx, *id)
|
||||
} else if nick != nil {
|
||||
return r.s.Characters.FindNick(ctx, *nick)
|
||||
} else {
|
||||
return nil, errors.New("You must specify either an ID or a nick")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *queryResolver) Characters(ctx context.Context, filter *models.CharacterFilter) ([]*models.Character, error) {
|
||||
if filter == nil {
|
||||
filter = &models.CharacterFilter{}
|
||||
}
|
||||
|
||||
return r.s.Characters.List(ctx, *filter)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddCharacter(ctx context.Context, input graphcore.CharacterAddInput) (*models.Character, error) {
|
||||
author := ""
|
||||
if input.Author != nil {
|
||||
author = *input.Author
|
||||
}
|
||||
|
||||
shortName := ""
|
||||
if input.ShortName != nil {
|
||||
shortName = *input.ShortName
|
||||
}
|
||||
|
||||
description := ""
|
||||
if input.Description != nil {
|
||||
description = *input.Description
|
||||
}
|
||||
|
||||
return r.s.Characters.Create(ctx, input.Nick, input.Name, shortName, author, description)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddCharacterNick(ctx context.Context, input graphcore.CharacterNickInput) (*models.Character, error) {
|
||||
return r.s.Characters.AddNick(ctx, input.ID, input.Nick)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveCharacterNick(ctx context.Context, input graphcore.CharacterNickInput) (*models.Character, error) {
|
||||
return r.s.Characters.RemoveNick(ctx, input.ID, input.Nick)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditCharacter(ctx context.Context, input graphcore.CharacterEditInput) (*models.Character, error) {
|
||||
return r.s.Characters.Update(ctx, input.ID, input.Name, input.ShortName, input.Description)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveCharacter(ctx context.Context, input graphcore.CharacterRemoveInput) (*models.Character, error) {
|
||||
return r.s.Characters.Delete(ctx, input.ID)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *queryResolver) Comment(ctx context.Context, id string) (*models.Comment, error) {
|
||||
return r.s.Stories.FindComment(ctx, id)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) AddComment(ctx context.Context, input graphcore.CommentAddInput) (*models.Comment, error) {
|
||||
chapter, err := r.s.Stories.FindChapter(ctx, input.ChapterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subject := ""
|
||||
if input.Subject != nil {
|
||||
subject = *input.Subject
|
||||
}
|
||||
|
||||
fictionalDate := time.Time{}
|
||||
if input.FictionalDate != nil {
|
||||
fictionalDate = *input.FictionalDate
|
||||
}
|
||||
|
||||
return r.s.Stories.CreateComment(ctx, *chapter, subject, "", input.Source, input.CharacterName, input.CharacterID, time.Now(), fictionalDate)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditComment(ctx context.Context, input graphcore.CommentEditInput) (*models.Comment, error) {
|
||||
comment, err := r.s.Stories.FindComment(ctx, input.CommentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fictionalDate := input.FictionalDate
|
||||
if input.ClearFictionalDate != nil && *input.ClearFictionalDate {
|
||||
fictionalDate = &time.Time{}
|
||||
}
|
||||
|
||||
return r.s.Stories.EditComment(ctx, comment, input.Source, input.CharacterName, input.CharacterID, input.Subject, fictionalDate)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveComment(ctx context.Context, input graphcore.CommentRemoveInput) (*models.Comment, error) {
|
||||
comment, err := r.s.Stories.FindComment(ctx, input.CommentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.s.Stories.RemoveComment(ctx, comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comment, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
// Queries
|
||||
|
||||
func (r *queryResolver) File(ctx context.Context, id string) (*models.File, error) {
|
||||
return r.s.Files.Find(ctx, id)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Files(ctx context.Context, filter *models.FileFilter) ([]*models.File, error) {
|
||||
if filter == nil {
|
||||
public := true
|
||||
|
||||
filter = &models.FileFilter{
|
||||
Public: &public,
|
||||
}
|
||||
}
|
||||
|
||||
return r.s.Files.List(ctx, *filter)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
func (r *mutationResolver) UploadFile(ctx context.Context, input *graphcore.FileUploadInput) (*models.File, error) {
|
||||
return r.s.Files.Upload(ctx, input.File.File, input.Name, input.Public, input.File.Size)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditFile(ctx context.Context, input *graphcore.FileEditInput) (*models.File, error) {
|
||||
return r.s.Files.Edit(ctx, input.ID, input.Name, input.Public)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveFile(ctx context.Context, input *graphcore.FileRemoveInput) (*models.File, error) {
|
||||
return r.s.Files.Remove(ctx, input.ID)
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
+23
-5
@@ -46,7 +46,7 @@ type Query {
|
||||
|
||||
|
||||
# Find all distinct tags used in stories
|
||||
tags: [Tag!]!
|
||||
tags(filter: TagFilter): [Tag!]!
|
||||
|
||||
|
||||
# Find story by ID
|
||||
@@ -107,7 +107,7 @@ type Mutation {
|
||||
"Edit a comment in a chapter."
|
||||
editComment(input: CommentEditInput!): Comment!
|
||||
|
||||
"Remove a comemnt in a chapter."
|
||||
"Remove a comment in a chapter."
|
||||
removeComment(input: CommentRemoveInput!): Comment!
|
||||
|
||||
|
||||
@@ -117,6 +117,12 @@ type Mutation {
|
||||
# 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!
|
||||
|
||||
@@ -158,15 +164,27 @@ type Mutation {
|
||||
|
||||
# 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(keys: [ChangeKeyInput!]!): Change!
|
||||
changes(filter: ChangesFilter): Change!
|
||||
}
|
||||
|
||||
# A Date represents a RFC3339 encoded date with up to millisecond precision.
|
||||
scalar Date
|
||||
# A Time represents a RFC3339 encoded date with up to millisecond precision.
|
||||
scalar Time
|
||||
|
||||
"""A GraphQL file upload."""
|
||||
scalar Upload
|
||||
@@ -21,13 +21,13 @@ type Change {
|
||||
keys: [ChangeKey!]!
|
||||
|
||||
"The date when the change happened."
|
||||
date: Date
|
||||
date: Time
|
||||
|
||||
"The changed objects."
|
||||
objects: [ChangeObject!]!
|
||||
}
|
||||
|
||||
union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter | Comment
|
||||
union ChangeObject = Character | Channel | Log | Post | Story | Tag | Chapter | Comment | File
|
||||
|
||||
"""
|
||||
A change key is a key associated with a change, used for filtering
|
||||
@@ -64,6 +64,8 @@ enum ChangeModel {
|
||||
Story
|
||||
Tag
|
||||
Chapter
|
||||
Comment
|
||||
File
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -73,9 +75,12 @@ input ChangesFilter {
|
||||
"The keys to query for."
|
||||
keys: [ChangeKeyInput!]
|
||||
|
||||
"Only show changes more recent than this date."
|
||||
earliestDate: Date
|
||||
"Only show changes by this author"
|
||||
author: String
|
||||
|
||||
"Limit the amount of results."
|
||||
"Only show changes more recent than this date."
|
||||
earliestDate: Time
|
||||
|
||||
"Limit the amount of results. This even goes for subscriptions!"
|
||||
limit: Int
|
||||
}
|
||||
@@ -13,13 +13,13 @@ type Chapter {
|
||||
source: String!
|
||||
|
||||
# When the chapter was posted initialy
|
||||
createdDate: Date!
|
||||
createdDate: Time!
|
||||
|
||||
# The in-universe date and time of the chapter
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
|
||||
# The date of edit.
|
||||
editedDate: Date!
|
||||
editedDate: Time!
|
||||
|
||||
"The comment mode."
|
||||
commentMode: ChapterCommentMode!
|
||||
@@ -69,7 +69,7 @@ input ChapterAddInput {
|
||||
source: String!
|
||||
|
||||
# Optionally, assign a fictional date for the chapter
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
|
||||
"The comment mode for the chapter."
|
||||
commentMode: ChapterCommentMode
|
||||
@@ -87,7 +87,7 @@ input ChapterEditInput {
|
||||
source: String
|
||||
|
||||
# Set the fictional date for a chapter
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
|
||||
# Remove the fictional date for a chapter
|
||||
clearFictionalDate: Boolean
|
||||
|
||||
@@ -21,13 +21,13 @@ type Comment {
|
||||
character: Character
|
||||
|
||||
"The fictional (IC) date of the comment."
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
|
||||
"The date of creation."
|
||||
createdDate: Date!
|
||||
createdDate: Time!
|
||||
|
||||
"The date of the last edit."
|
||||
editedDate: Date!
|
||||
editedDate: Time!
|
||||
|
||||
"The markdown source of the comment."
|
||||
source: String!
|
||||
@@ -50,7 +50,7 @@ input CommentAddInput {
|
||||
characterId: String
|
||||
|
||||
"Optional in-universe date of the comment."
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
|
||||
"Optional subject to add to the comment, only shown in some modes."
|
||||
subject: String
|
||||
@@ -73,7 +73,7 @@ input CommentEditInput {
|
||||
characterId: String
|
||||
|
||||
"Change the optional in-universe date of the comment."
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
|
||||
"Set to clear the fictional date."
|
||||
clearFictionalDate: Boolean
|
||||
|
||||
@@ -7,7 +7,7 @@ type File {
|
||||
kind: String!
|
||||
|
||||
# The time of uploading
|
||||
time: Date!
|
||||
time: Time!
|
||||
|
||||
# Whether the file is publicly listable. Someone with knowledge of the ID
|
||||
# will still be able to view it, however.
|
||||
@@ -35,12 +35,24 @@ input FilesFilter {
|
||||
public: Boolean
|
||||
|
||||
# Limit the MIME types of the files.
|
||||
mimeType: [String!]
|
||||
mimeTypes: [String!]
|
||||
}
|
||||
|
||||
# Input for uploadFile mutation
|
||||
input FileUploadInput {
|
||||
"The file name"
|
||||
name: String!
|
||||
|
||||
# Whether the file should be public
|
||||
public: Boolean!
|
||||
|
||||
# The actual file upload
|
||||
file: Upload!
|
||||
}
|
||||
|
||||
# Input for editFile mutation
|
||||
input 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
|
||||
@@ -49,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: Date!
|
||||
date: Time!
|
||||
|
||||
# The channel of a log.
|
||||
channelName: String!
|
||||
@@ -34,6 +34,20 @@ 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!]!
|
||||
}
|
||||
|
||||
# Suggested log
|
||||
type LogSuggestion {
|
||||
log: Log!
|
||||
hasEvent: Boolean!
|
||||
hasChannel: Boolean!
|
||||
characters: [Character!]!
|
||||
}
|
||||
|
||||
# Filter for logs query
|
||||
@@ -60,7 +74,7 @@ input LogsFilter {
|
||||
# Input for addLog mutation
|
||||
input LogAddInput {
|
||||
# The date of the log.
|
||||
date: Date!
|
||||
date: Time!
|
||||
|
||||
# The channel where the log is set
|
||||
channel: String!
|
||||
@@ -116,12 +130,37 @@ input LogImportInput {
|
||||
timezone: String
|
||||
|
||||
"The date of the log, if not provided in the log body."
|
||||
date: Date
|
||||
date: Time
|
||||
|
||||
"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.
|
||||
|
||||
@@ -7,7 +7,7 @@ type Post {
|
||||
logId: String!
|
||||
|
||||
# The date and time of posting
|
||||
time: Date!
|
||||
time: Time!
|
||||
|
||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||
kind: String!
|
||||
@@ -28,7 +28,7 @@ input PostAddInput {
|
||||
logId: String!
|
||||
|
||||
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
||||
time: Date!
|
||||
time: Time!
|
||||
|
||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||
kind: String!
|
||||
@@ -46,7 +46,7 @@ input PostEditInput {
|
||||
id: String!
|
||||
|
||||
# The date and time of posting, in a RFC3339 format with up to a nanosecond's precision
|
||||
time: Date
|
||||
time: Time
|
||||
|
||||
# The kind of post this is. Only "text", "scene" and "action" are RP, while others are annotations and 'commands'.
|
||||
kind: String
|
||||
@@ -75,8 +75,8 @@ input PostRemoveInput {
|
||||
|
||||
# Filter for posts query
|
||||
input PostsFilter {
|
||||
id: [String!]
|
||||
kind: [String!]
|
||||
ids: [String!]
|
||||
kinds: [String!]
|
||||
logId: String
|
||||
limit: Int
|
||||
}
|
||||
@@ -26,13 +26,13 @@ type Story {
|
||||
chapters: [Chapter!]!
|
||||
|
||||
# The date the story was created.
|
||||
createdDate: Date!
|
||||
createdDate: Time!
|
||||
|
||||
# The date the story is set in.
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
|
||||
# The date of the last major update to the story. This being bumped means a new chapter has been added.
|
||||
updatedDate: Date!
|
||||
updatedDate: Time!
|
||||
}
|
||||
|
||||
# Filter for stories query.
|
||||
@@ -53,10 +53,10 @@ input StoriesFilter {
|
||||
open: Boolean
|
||||
|
||||
# The earliest fictionalDate
|
||||
earliestFictionalDate: Date
|
||||
earliestFictionalDate: Time
|
||||
|
||||
# The latest fictionalDate
|
||||
latestFictionalDate: Date
|
||||
latestFictionalDate: Time
|
||||
|
||||
# The max amount of stories to get (default: 30)
|
||||
limit: Int
|
||||
@@ -85,7 +85,7 @@ input StoryAddInput {
|
||||
tags: [TagInput!]
|
||||
|
||||
# Set the fictional date of the story.
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
}
|
||||
|
||||
# Input for the editStory mutation
|
||||
@@ -111,7 +111,7 @@ input StoryEditInput {
|
||||
listed: Boolean
|
||||
|
||||
# Set the fictional date of the story.
|
||||
fictionalDate: Date
|
||||
fictionalDate: Time
|
||||
|
||||
# Clear the fictional date of the story.
|
||||
clearFictionalDate: Boolean
|
||||
|
||||
@@ -16,6 +16,15 @@ input TagInput {
|
||||
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...
|
||||
|
||||
@@ -4,18 +4,18 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"git.aiterp.net/rpdata/api/graph2/input"
|
||||
"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) ([]input.ChangeObject, error) {
|
||||
func (r *changeResolver) Objects(ctx context.Context, obj *models.Change) ([]graphcore.ChangeObject, error) {
|
||||
objects := obj.Objects()
|
||||
|
||||
results := make([]input.ChangeObject, 0, len(objects))
|
||||
results := make([]graphcore.ChangeObject, 0, len(objects))
|
||||
for _, object := range objects {
|
||||
if changeObject, ok := object.(input.ChangeObject); ok {
|
||||
if changeObject, ok := object.(graphcore.ChangeObject); ok {
|
||||
results = append(results, changeObject)
|
||||
} else {
|
||||
log.Printf("Type %T needs ChangeObject impl", object)
|
||||
|
||||
+11
-26
@@ -2,14 +2,15 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.aiterp.net/rpdata/api/services"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/comments"
|
||||
)
|
||||
|
||||
type chapterResolver struct{}
|
||||
type chapterResolver struct {
|
||||
stories *services.StoryService
|
||||
}
|
||||
|
||||
func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Chapter) (*time.Time, error) {
|
||||
if chapter.FictionalDate.IsZero() {
|
||||
@@ -20,31 +21,15 @@ func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Cha
|
||||
}
|
||||
|
||||
func (r *chapterResolver) Comments(ctx context.Context, chapter *models.Chapter, limit *int) ([]*models.Comment, error) {
|
||||
limitValue := 0
|
||||
if limit != nil {
|
||||
if *limit < 0 {
|
||||
return nil, errors.New("Limit cannot be negative")
|
||||
}
|
||||
|
||||
limitValue = *limit
|
||||
if limit == nil {
|
||||
limit = new(int)
|
||||
*limit = 0
|
||||
}
|
||||
|
||||
if !chapter.CommentMode.IsEnabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
comments, err := comments.ListChapterID(chapter.ID, limitValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comments2 := make([]*models.Comment, len(comments))
|
||||
for i := range comments {
|
||||
comments2[i] = &comments[i]
|
||||
}
|
||||
|
||||
return comments2, nil
|
||||
return r.stories.ListComments(ctx, *chapter, *limit)
|
||||
}
|
||||
|
||||
// ChapterResolver is a resolver
|
||||
var ChapterResolver chapterResolver
|
||||
func ChapterResolver(s *services.Bundle) *chapterResolver {
|
||||
return &chapterResolver{stories: s.Stories}
|
||||
}
|
||||
|
||||
+15
-11
@@ -2,31 +2,33 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.aiterp.net/rpdata/api/repositories"
|
||||
"git.aiterp.net/rpdata/api/services"
|
||||
"github.com/globalsign/mgo"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/loader"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
type commentResolver struct{}
|
||||
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
|
||||
}
|
||||
|
||||
loader := loader.FromContext(ctx)
|
||||
if loader == nil {
|
||||
return nil, errors.New("no loader")
|
||||
}
|
||||
|
||||
character, err := loader.Character("id", obj.CharacterID)
|
||||
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
|
||||
return character, nil
|
||||
}
|
||||
|
||||
func (r *commentResolver) FictionalDate(ctx context.Context, obj *models.Comment) (*time.Time, error) {
|
||||
@@ -38,4 +40,6 @@ func (r *commentResolver) FictionalDate(ctx context.Context, obj *models.Comment
|
||||
}
|
||||
|
||||
// CommentResolver is a resolver
|
||||
var CommentResolver commentResolver
|
||||
func CommentResolver(s *services.Bundle) *commentResolver {
|
||||
return &commentResolver{characters: s.Characters}
|
||||
}
|
||||
|
||||
+22
-34
@@ -2,56 +2,44 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.aiterp.net/rpdata/api/services"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/loader"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/posts"
|
||||
)
|
||||
|
||||
type logResolver struct{}
|
||||
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) {
|
||||
loader := loader.FromContext(ctx)
|
||||
if loader == nil {
|
||||
return nil, errors.New("no loader")
|
||||
}
|
||||
|
||||
return loader.Channel("name", log.ChannelName)
|
||||
return r.channels.Find(ctx, log.ChannelName)
|
||||
}
|
||||
|
||||
func (r *logResolver) Characters(ctx context.Context, log *models.Log) ([]*models.Character, error) {
|
||||
loader := loader.FromContext(ctx)
|
||||
if loader == nil {
|
||||
return nil, errors.New("no loader")
|
||||
if len(log.CharacterIDs) == 0 {
|
||||
return []*models.Character{}, nil
|
||||
}
|
||||
|
||||
characters, err := loader.Characters("id", log.CharacterIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
characters2 := make([]*models.Character, len(characters))
|
||||
for i := range characters {
|
||||
characters2[i] = &characters[i]
|
||||
}
|
||||
|
||||
return characters2, 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) {
|
||||
posts, err := posts.List(&posts.Filter{LogID: &log.ShortID, Kind: kinds, Limit: 0})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.logs.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
|
||||
}
|
||||
|
||||
posts2 := make([]*models.Post, len(posts))
|
||||
for i := range posts {
|
||||
posts2[i] = &posts[i]
|
||||
}
|
||||
func (r *logResolver) NextLogs(ctx context.Context, obj *models.Log) ([]*models.LogSuggestion, error) {
|
||||
return r.logs.NextLogs(ctx, obj)
|
||||
}
|
||||
|
||||
return posts2, nil
|
||||
func (r *logResolver) PrevLogs(ctx context.Context, obj *models.Log) ([]*models.LogSuggestion, error) {
|
||||
return r.logs.PrevLogs(ctx, obj)
|
||||
}
|
||||
|
||||
// LogResolver is a resolver
|
||||
var LogResolver logResolver
|
||||
func LogResolver(s *services.Bundle) *logResolver {
|
||||
return &logResolver{characters: s.Characters, logs: s.Logs, channels: s.Channels}
|
||||
}
|
||||
|
||||
+8
-15
@@ -2,14 +2,15 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/services"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models/chapters"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
type storyResolver struct{}
|
||||
type storyResolver struct {
|
||||
stories *services.StoryService
|
||||
}
|
||||
|
||||
func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story) (*time.Time, error) {
|
||||
if story.FictionalDate.IsZero() {
|
||||
@@ -20,18 +21,10 @@ func (r *storyResolver) FictionalDate(ctx context.Context, story *models.Story)
|
||||
}
|
||||
|
||||
func (r *storyResolver) Chapters(ctx context.Context, story *models.Story) ([]*models.Chapter, error) {
|
||||
chapters, err := chapters.ListStoryID(story.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chapters2 := make([]*models.Chapter, len(chapters))
|
||||
for i := range chapters {
|
||||
chapters2[i] = &chapters[i]
|
||||
}
|
||||
|
||||
return chapters2, nil
|
||||
return r.stories.ListChapters(ctx, *story)
|
||||
}
|
||||
|
||||
// StoryResolver is a resolver
|
||||
var StoryResolver storyResolver
|
||||
func StoryResolver(s *services.Bundle) *storyResolver {
|
||||
return &storyResolver{stories: s.Stories}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,20 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata/api/services"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/users"
|
||||
)
|
||||
|
||||
type tokenResolver struct{}
|
||||
type tokenResolver struct {
|
||||
auth *services.AuthService
|
||||
}
|
||||
|
||||
func (r *tokenResolver) User(ctx context.Context, token *models.Token) (*models.User, error) {
|
||||
user, err := users.Find(token.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
return r.auth.FindUser(ctx, token.UserID)
|
||||
}
|
||||
|
||||
// TokenResolver is a resolver
|
||||
var TokenResolver tokenResolver
|
||||
func TokenResolver(s *services.Bundle) *tokenResolver {
|
||||
return &tokenResolver{auth: s.Auth}
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
var keyCollection *mgo.Collection
|
||||
|
||||
// A Key contains a JWT secret and the limitations of it. There are two types of
|
||||
// keys, single-user keys and wildcard keys. The former is used to authenticate
|
||||
// a single user (e.g. the logbot) through an API while the latter is only for
|
||||
// services that can be trusted to perform its own authentication (a frontend).
|
||||
type Key struct {
|
||||
ID string `bson:"_id"`
|
||||
Name string `bson:"name"`
|
||||
User string `bson:"user"`
|
||||
Secret string `bson:"secret"`
|
||||
}
|
||||
|
||||
// ValidForUser returns true if the key's user is the same as
|
||||
// the user, or it's a wildcard key.
|
||||
func (key *Key) ValidForUser(user string) bool {
|
||||
return key.User == user || key.User == "*"
|
||||
}
|
||||
|
||||
// FindKey finds a key by kid (key ID)
|
||||
func FindKey(kid string) (Key, error) {
|
||||
key := Key{}
|
||||
err := keyCollection.FindId(kid).One(&key)
|
||||
|
||||
return key, err
|
||||
}
|
||||
|
||||
// NewKey generates a new key for the user and name. This
|
||||
// does not allow generating wildcard keys, they have to be
|
||||
// manually inserted into the DB.
|
||||
func NewKey(name, user string) (*Key, error) {
|
||||
if user == "*" {
|
||||
return nil, errors.New("auth: wildcard keys not allowed")
|
||||
}
|
||||
|
||||
secret, err := makeKeySecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := &Key{
|
||||
ID: makeKeyID(),
|
||||
Name: name,
|
||||
User: user,
|
||||
Secret: secret,
|
||||
}
|
||||
|
||||
if err := keyCollection.Insert(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
keyCollection = db.C("auth.keys")
|
||||
|
||||
keyCollection.EnsureIndexKey("user")
|
||||
})
|
||||
}
|
||||
|
||||
// makeKeyID makes a random story ID that's 16 characters long
|
||||
func makeKeyID() string {
|
||||
result := "K"
|
||||
offset := 0
|
||||
data := make([]byte, 32)
|
||||
|
||||
rand.Read(data)
|
||||
for len(result) < 16 {
|
||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
||||
offset += 8
|
||||
|
||||
if offset >= 32 {
|
||||
rand.Read(data)
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
return result[:16]
|
||||
}
|
||||
|
||||
func makeKeySecret() (string, error) {
|
||||
data := make([]byte, 64)
|
||||
alphabet := "0123456789abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSUTVWXYZ-_"
|
||||
|
||||
_, err := rand.Read(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := range data {
|
||||
data[i] = alphabet[data[i]%64]
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package auth
|
||||
|
||||
// AllPermissions gets all permissions and their purpose
|
||||
func AllPermissions() map[string]string {
|
||||
return map[string]string{
|
||||
"member": "Can add/edit/remove own content",
|
||||
"user.edit": "Can edit non-owned users",
|
||||
"character.add": "Can add non-owned characters",
|
||||
"character.edit": "Can edit non-owned characters",
|
||||
"character.remove": "Can remove non-owned characters",
|
||||
"channel.add": "Can add channels",
|
||||
"channel.edit": "Can edit channels",
|
||||
"channel.remove": "Can remove channels",
|
||||
"log.add": "Can add logs",
|
||||
"log.edit": "Can edit logs",
|
||||
"log.remove": "Can remove logs",
|
||||
"post.add": "Can add posts",
|
||||
"post.edit": "Can edit posts",
|
||||
"post.move": "Can move posts",
|
||||
"post.remove": "Can remove posts",
|
||||
"story.add": "Can add non-owned stories",
|
||||
"story.edit": "Can edit non-owned stories",
|
||||
"story.remove": "Can remove non-owned stories",
|
||||
"story.unlisted": "Can view non-owned unlisted stories",
|
||||
"file.upload": "Can upload files",
|
||||
"file.edit": "Can edit non-owned files",
|
||||
"file.remove": "Can remove non-owned files",
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/users"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
var contextKey = &struct{ data string }{"Token Context Key"}
|
||||
|
||||
// ErrNoKid is returned if the key id is missing from the jwt token header,
|
||||
var ErrNoKid = errors.New("Missing \"kid\" field in token")
|
||||
|
||||
// ErrKeyNotFound is returned if the key wasn't found.
|
||||
var ErrKeyNotFound = errors.New("Key not found")
|
||||
|
||||
// ErrInvalidClaims is returned by parseClaims if the claims cannot be parsed
|
||||
var ErrInvalidClaims = errors.New("Invalid claims in token")
|
||||
|
||||
// ErrExpired is returned by parseClaims if the expiry date is in the past
|
||||
var ErrExpired = errors.New("Claims have already expired")
|
||||
|
||||
// ErrWrongUser is returned by CheckToken if the key cannot represent this user
|
||||
var ErrWrongUser = errors.New("Key is not valid for this user")
|
||||
|
||||
// ErrWrongPermissions is returned by CheckToken if the key cannot claim one or more of its permissions
|
||||
var ErrWrongPermissions = errors.New("User does not have these permissions")
|
||||
|
||||
// ErrDeletedUser is returned by CheckToken if the key can represent this user, but the user doesn't exist.
|
||||
var ErrDeletedUser = errors.New("User was not found")
|
||||
|
||||
// TokenFromContext gets the token from context.
|
||||
func TokenFromContext(ctx context.Context) *models.Token {
|
||||
token, ok := ctx.Value(contextKey).(*models.Token)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// RequestWithToken either returns the request, or the request with a new context that
|
||||
// has the token.
|
||||
func RequestWithToken(r *http.Request) *http.Request {
|
||||
header := r.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return r
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(header, "Bearer ") {
|
||||
return r
|
||||
}
|
||||
|
||||
token, err := CheckToken(header[7:])
|
||||
if err != nil {
|
||||
return r
|
||||
}
|
||||
|
||||
return r.WithContext(context.WithValue(r.Context(), contextKey, &token))
|
||||
}
|
||||
|
||||
// CheckToken reads the token string and returns a token if everything is kosher.
|
||||
func CheckToken(tokenString string) (token models.Token, err error) {
|
||||
var key Key
|
||||
|
||||
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
|
||||
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
|
||||
}
|
||||
|
||||
kid, ok := jwtToken.Header["kid"].(string)
|
||||
if !ok {
|
||||
return nil, ErrNoKid
|
||||
}
|
||||
|
||||
key, err = FindKey(kid)
|
||||
if err != nil || key.ID == "" {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
|
||||
return []byte(key.Secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return models.Token{}, err
|
||||
}
|
||||
|
||||
userid, permissions, err := parseClaims(jwtToken.Claims)
|
||||
if err != nil {
|
||||
return models.Token{}, err
|
||||
}
|
||||
|
||||
if !key.ValidForUser(userid) {
|
||||
return models.Token{}, ErrWrongUser
|
||||
}
|
||||
|
||||
user, err := users.Ensure(userid)
|
||||
if err != nil {
|
||||
return models.Token{}, ErrDeletedUser
|
||||
}
|
||||
|
||||
acceptedPermissions := make([]string, 0, 8)
|
||||
for _, permission := range permissions {
|
||||
found := false
|
||||
|
||||
for _, userPermission := range user.Permissions {
|
||||
if permission == userPermission {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
acceptedPermissions = append(acceptedPermissions, permission)
|
||||
}
|
||||
}
|
||||
|
||||
return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil
|
||||
}
|
||||
|
||||
func parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err error) {
|
||||
mapClaims, ok := jwtClaims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return "", nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
if !mapClaims.VerifyExpiresAt(time.Now().Unix(), true) {
|
||||
return "", nil, ErrExpired
|
||||
}
|
||||
|
||||
if userid, ok = mapClaims["user"].(string); !ok {
|
||||
return "", nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
if claimedPermissions, ok := mapClaims["permissions"].([]interface{}); ok {
|
||||
for _, permission := range claimedPermissions {
|
||||
if permission, ok := permission.(string); ok {
|
||||
permissions = append(permissions, permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(permissions) == 0 {
|
||||
return "", nil, ErrInvalidClaims
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+53
-26
@@ -5,56 +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 {
|
||||
Enabled bool `json:"enabled"`
|
||||
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")
|
||||
@@ -65,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)
|
||||
}
|
||||
|
||||
@@ -1,55 +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
|
||||
}
|
||||
|
||||
// NextMany gets the next value of a counter, or an error if it hasn't, and increments by a specified value.
|
||||
// Any value `returned` to `returned+(increment-1)` should then be safe to use.
|
||||
func NextMany(category, name string, increment int) (int, error) {
|
||||
id := category + "." + name
|
||||
doc := counter{}
|
||||
|
||||
_, err := collection.Find(bson.M{"_id": id}).Apply(mgo.Change{
|
||||
Update: bson.M{"$inc": bson.M{"value": increment}},
|
||||
Upsert: true,
|
||||
ReturnNew: true,
|
||||
}, &doc)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return doc.Value, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
collection = db.C("core.counters")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
mrand "math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ID generates an ID using crypto-random, falling back to math random when that fails
|
||||
// to avoid disrupting operation because of a faulty RNG.
|
||||
func ID(prefix string, length int) string {
|
||||
var data [32]byte
|
||||
|
||||
result := strings.Builder{}
|
||||
result.Grow(length + 32)
|
||||
result.WriteString(prefix)
|
||||
|
||||
pos := 0
|
||||
for result.Len() < length {
|
||||
if pos == 0 {
|
||||
randRead(data[:])
|
||||
}
|
||||
|
||||
result.WriteString(strconv.FormatUint(binary.BigEndian.Uint64(data[pos:pos+8]), 36))
|
||||
|
||||
pos = (pos + 8) % 32
|
||||
}
|
||||
|
||||
return result.String()[:length]
|
||||
}
|
||||
|
||||
func randRead(data []byte) {
|
||||
n, err := rand.Read(data)
|
||||
if err != nil {
|
||||
mrand.Read(data[n:])
|
||||
}
|
||||
}
|
||||
|
||||
// PostID generates a post ID.
|
||||
func PostID() string {
|
||||
return ID("P", 16)
|
||||
}
|
||||
|
||||
// StoryID generates a post ID.
|
||||
func StoryID() string {
|
||||
return ID("S", 16)
|
||||
}
|
||||
|
||||
// ChapterID generates a post ID.
|
||||
func ChapterID() string {
|
||||
return ID("SC", 24)
|
||||
}
|
||||
|
||||
// CommentID generates a post ID.
|
||||
func CommentID() string {
|
||||
return ID("SCC", 32)
|
||||
}
|
||||
|
||||
// FileID generates a file ID.
|
||||
func FileID() string {
|
||||
return ID("F", 16)
|
||||
}
|
||||
|
||||
// FileID generates a file ID.
|
||||
func FileUploadID() string {
|
||||
return ID("U", 24)
|
||||
}
|
||||
|
||||
func KeyID() string {
|
||||
return ID("K", 32)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogID generates a log ID in the format is 2019-05-22_191341547_RedrockAgency
|
||||
func LogID(log models.Log) string {
|
||||
if len(log.ChannelName) < 1 {
|
||||
panic("ChannelName is not valid (validate input before calling this function!)")
|
||||
}
|
||||
|
||||
datetime := log.Date.UTC().Format("2006-01-02_150405")
|
||||
milliseconds := (log.Date.UnixNano() % int64(time.Second)) / 1000000
|
||||
channelName := log.ChannelName[1:]
|
||||
|
||||
return fmt.Sprintf("%s%03d_%s", datetime, milliseconds, channelName)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package forumlog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseMetadata parses metadata, discards the broken parts, and returns the
|
||||
// parsed data as a map (`m`) and the position of the first IRC post (`n`)
|
||||
func ParseMetadata(data string) map[string][]string {
|
||||
result := make(map[string][]string)
|
||||
key := ""
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(data))
|
||||
for scanner.Scan() {
|
||||
line := strings.Trim(scanner.Text(), "\r\t ")
|
||||
|
||||
if strings.HasPrefix(line, "[") {
|
||||
break
|
||||
}
|
||||
if len(line) < 1 {
|
||||
key = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
split := strings.Split(line, ":")
|
||||
key = split[0]
|
||||
} else {
|
||||
result[key] = append(result[key], line)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package forumlog_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/importers/forumlog"
|
||||
)
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
data := forumlog.ParseMetadata(testData1)
|
||||
|
||||
assert.Equal(t, data["Date"], []string{
|
||||
"August 11, 2014",
|
||||
"August 12, 2014",
|
||||
"August 13, 2014",
|
||||
})
|
||||
assert.Equal(t, data["Location"], []string{
|
||||
"Aroste, an island off the Derraian coastline.",
|
||||
})
|
||||
assert.Equal(t, data["GM"], []string{
|
||||
"Gisle",
|
||||
})
|
||||
assert.Equal(t, data["Length"], []string{
|
||||
"847 posts",
|
||||
})
|
||||
assert.Equal(t, len(data["Stuff"]), 0)
|
||||
|
||||
for key := range data {
|
||||
if strings.HasPrefix(key, "[") {
|
||||
t.Error("It should have stopped at the IRC post, yet this key exists:", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testData1 = `
|
||||
Date: August 2185
|
||||
August 11, 2014
|
||||
August 12, 2014
|
||||
August 13, 2014
|
||||
|
||||
Location:
|
||||
Aroste, an island off the Derraian coastline.
|
||||
|
||||
GM:
|
||||
Gisle
|
||||
|
||||
Length:
|
||||
847 posts
|
||||
|
||||
Characters:
|
||||
Calyx Vadris - Osiris
|
||||
Damien Monroe - Bowe
|
||||
Jason Wolfe - Dante
|
||||
Renala T'Iavay - Gisle
|
||||
Victoria Steels - MCB280
|
||||
|
||||
NPCs:
|
||||
Recurring:
|
||||
Halisi - Tyranniac
|
||||
Jattic - Dante
|
||||
Jelvan Darennon - Gisle
|
||||
Leah - Dante
|
||||
Marissa T'Evin - Gisle
|
||||
Philip Lacour - Tyranniac
|
||||
|
||||
One-off:
|
||||
Reidas Falten (Turian - Guard meeting them on their trip to the building)
|
||||
Sarian T'Dera (Asari - Asari opening fire on Calyx, Leah and Renala)
|
||||
Mariam Adams (Human - The receptionist.)
|
||||
Prerix Falten (Turian2 - Victoria's chair)
|
||||
Jonathan Lyng (OtherHuman - The one opening fire on Leah entering the garage)
|
||||
Alerena Teris (Teris - Asari met in the maintenance corridor)
|
||||
|
||||
<-- Redorck Agency - August 11, 2014
|
||||
|
||||
[13:19] * Leah looks over at Damien. "Haven't seen you around the agency." she says curiously, "New hire?"
|
||||
|
||||
`
|
||||
@@ -1,48 +0,0 @@
|
||||
package mirclike
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
// ErrEmptyLog is returned by ParseLog if there are no (valid) posts in the log.
|
||||
var ErrEmptyLog = errors.New("No valid posts found in log")
|
||||
|
||||
// ParseLog parses the log and returns the things that can be gleamed from them.
|
||||
func ParseLog(data string, date time.Time, strict bool) (models.Log, []models.Post, error) {
|
||||
lines := strings.Split(data, "\n")
|
||||
posts := make([]models.Post, 0, len(lines))
|
||||
prev := models.Post{}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.Trim(line, "\r\t ")
|
||||
if len(line) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
post, err := ParsePost(line, date, prev)
|
||||
if err != nil {
|
||||
if strict {
|
||||
return models.Log{}, nil, err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
posts = append(posts, post)
|
||||
prev = post
|
||||
}
|
||||
|
||||
if len(posts) == 0 {
|
||||
return models.Log{}, nil, ErrEmptyLog
|
||||
}
|
||||
|
||||
log := models.Log{
|
||||
Date: posts[0].Time,
|
||||
}
|
||||
|
||||
return log, posts, nil
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package mirclike_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/importers/mirclike"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
var testLog = strings.Join([]string{
|
||||
"[11:21] * Va`ynna_Atana returns to the apartment at the end of another long day. She hangs up her jacket and a green scarf next to the door before going straight to the bathroom; she leaves the door open, though. She walked home with Uvena this time, but the two parted ways downstairs.",
|
||||
"",
|
||||
"[11:21] <=Scene=> The weather outside is not pleasant in the slightest. The storm is still going on, visibility is reduced and the worst gusts of wind can be felt inside the apartment as a faint shudder. ",
|
||||
"",
|
||||
"[11:",
|
||||
" [11:27] <Test> Stuff and things.",
|
||||
}, "\r\n")
|
||||
|
||||
var testLogPosts = []models.Post{
|
||||
{
|
||||
ID: "UNASSIGNED",
|
||||
LogID: "UNASSIGNED",
|
||||
Time: parseDate(nil, "2018-05-11 11:21:00"),
|
||||
Kind: "action",
|
||||
Nick: "Va`ynna_Atana",
|
||||
Position: 1,
|
||||
Text: "returns to the apartment at the end of another long day. She hangs up her jacket and a green scarf next to the door before going straight to the bathroom; she leaves the door open, though. She walked home with Uvena this time, but the two parted ways downstairs.",
|
||||
},
|
||||
{
|
||||
ID: "UNASSIGNED",
|
||||
LogID: "UNASSIGNED",
|
||||
Time: parseDate(nil, "2018-05-11 11:21:00"),
|
||||
Kind: "scene",
|
||||
Nick: "=Scene=",
|
||||
Position: 2,
|
||||
Text: "The weather outside is not pleasant in the slightest. The storm is still going on, visibility is reduced and the worst gusts of wind can be felt inside the apartment as a faint shudder.",
|
||||
},
|
||||
{
|
||||
ID: "UNASSIGNED",
|
||||
LogID: "UNASSIGNED",
|
||||
Time: parseDate(nil, "2018-05-11 11:27:00"),
|
||||
Kind: "text",
|
||||
Nick: "Test",
|
||||
Position: 3,
|
||||
Text: "Stuff and things.",
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseLog(t *testing.T) {
|
||||
log, posts, err := mirclike.ParseLog(testLog, parseDate(t, "2018-05-11 00:00:00"), false)
|
||||
if err != nil {
|
||||
t.Fatal("ParseLog", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, testLogPosts, posts)
|
||||
assert.Equal(t, posts[0].Time, log.Date, "Log's date should be the first post's.")
|
||||
}
|
||||
|
||||
func TestParseLogErrors(t *testing.T) {
|
||||
_, _, err1 := mirclike.ParseLog("\n\n\n\n\n \n\t\r\n", time.Time{}, false)
|
||||
_, _, err2 := mirclike.ParseLog("\n\n\n\n\n[14:57]* Stuff \n\t\r\n", time.Time{}, true)
|
||||
|
||||
assert.Equal(t, mirclike.ErrEmptyLog, err1)
|
||||
assert.Equal(t, mirclike.ErrNotPost, err2)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package mirclike_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/importers/mirclike"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
func TestParsePost(t *testing.T) {
|
||||
table := []struct {
|
||||
Input string
|
||||
TS string
|
||||
Kind string
|
||||
Nick string
|
||||
Text string
|
||||
}{
|
||||
{
|
||||
"[12:34] * Stuff does things.",
|
||||
"12:34:00", "action", "Stuff", "does things.",
|
||||
},
|
||||
{
|
||||
"[12:34] <Stuff> Things said.",
|
||||
"12:34:00", "text", "Stuff", "Things said.",
|
||||
},
|
||||
{
|
||||
"[13:36:59] <Stuff> Things said.",
|
||||
"13:36:59", "text", "Stuff", "Things said.",
|
||||
},
|
||||
{
|
||||
"[23:59] <=Scene=> Scenery and such.",
|
||||
"23:59:00", "scene", "=Scene=", "Scenery and such.",
|
||||
},
|
||||
{
|
||||
"[01:10:11] * =Scene= Scenery and such from the forum or mIRC using my old script.",
|
||||
"01:10:11", "scene", "=Scene=", "Scenery and such from the forum or mIRC using my old script.",
|
||||
},
|
||||
}
|
||||
|
||||
for i, row := range table {
|
||||
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) {
|
||||
post, err := mirclike.ParsePost(row.Input, time.Now(), models.Post{})
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse post:", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, row.TS, post.Time.Format("15:04:05"), "Timestamps should match.")
|
||||
assert.Equal(t, row.Kind, post.Kind, "Kinds should match.")
|
||||
assert.Equal(t, row.Nick, post.Nick, "Kinds should match.")
|
||||
assert.Equal(t, row.Text, post.Text, "Kinds should match.")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePostErrors(t *testing.T) {
|
||||
table := []struct {
|
||||
Input string
|
||||
Err error
|
||||
}{
|
||||
{"[12:34] <Stuff> Things said.", nil},
|
||||
{"[12:34] >Stuff> Things said.", mirclike.ErrNotPost},
|
||||
{"12:34] <Stuff> Things said.", mirclike.ErrNotPost},
|
||||
{"* Stuff Things said.", mirclike.ErrNotPost},
|
||||
{"", mirclike.ErrNotPost},
|
||||
{"[12:34 <Stuff> Things said.", mirclike.ErrNotPost},
|
||||
{"[TE:XT] <Stuff> Things said.", mirclike.ErrNotPost},
|
||||
{"[10] <Stuff> Things said.", mirclike.ErrNotPost},
|
||||
{"[12:34:56:789] <Stuff> Things said.", nil},
|
||||
{"[12:34:56.789] <Stuff> Things said.", mirclike.ErrNotPost},
|
||||
{"[12:34] <Stuff>", mirclike.ErrNotPost},
|
||||
{"[12:34] * Stuff", mirclike.ErrNotPost},
|
||||
{"[12:34] =Scene=", mirclike.ErrNotPost},
|
||||
{"[12:34] <=Scene=>", mirclike.ErrNotPost},
|
||||
}
|
||||
|
||||
for i, row := range table {
|
||||
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) {
|
||||
_, err := mirclike.ParsePost(row.Input, time.Now(), models.Post{})
|
||||
|
||||
assert.Equal(t, row.Err, err, "Error should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNextDay(t *testing.T) {
|
||||
table := []struct {
|
||||
Prev time.Time
|
||||
TS string
|
||||
Time time.Time
|
||||
}{
|
||||
{Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "12:01", Time: parseDate(t, "2019-01-12 12:01:00")},
|
||||
{Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "11:53:13", Time: parseDate(t, "2019-01-12 11:53:13")},
|
||||
{Prev: parseDate(t, "2019-04-08 23:51:59"), TS: "00:09", Time: parseDate(t, "2019-04-09 00:09:00")},
|
||||
{Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "11:29:59", Time: parseDate(t, "2019-01-13 11:29:59")},
|
||||
}
|
||||
|
||||
for i, row := range table {
|
||||
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) {
|
||||
input := fmt.Sprintf("[%s] * Stuff does things.", row.TS)
|
||||
|
||||
post, err := mirclike.ParsePost(input, row.Prev, models.Post{Time: row.Prev})
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse post:", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, row.Time, post.Time)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func parseDate(t *testing.T, date string) time.Time {
|
||||
result, err := time.Parse("2006-01-02 15:04:05", date)
|
||||
if err != nil {
|
||||
if t != nil {
|
||||
t.Fatal("Could not parse date", date, err)
|
||||
} else {
|
||||
panic("Could not parse date: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func formatDate(date time.Time) string {
|
||||
return date.UTC().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/channels"
|
||||
"github.com/graph-gophers/dataloader"
|
||||
)
|
||||
|
||||
// Channel gets a character by key
|
||||
func (loader *Loader) Channel(key, value string) (*models.Channel, error) {
|
||||
if !strings.HasPrefix(key, "Channel.") {
|
||||
key = "Channel." + key
|
||||
}
|
||||
|
||||
if loader.loaders[key] == nil {
|
||||
return nil, errors.New("unsupported key")
|
||||
}
|
||||
|
||||
loader.loadPrimed(key)
|
||||
|
||||
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
|
||||
res, err := thunk()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channel, ok := res.(models.Channel)
|
||||
if !ok {
|
||||
return nil, errors.New("incorrect type")
|
||||
}
|
||||
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
// PrimeChannels primes channels for loading along with the first one.
|
||||
func (loader *Loader) PrimeChannels(key string, values ...string) {
|
||||
if !strings.HasPrefix(key, "Channel.") {
|
||||
key = "Channel." + key
|
||||
}
|
||||
|
||||
loader.prime(key, values)
|
||||
}
|
||||
|
||||
func channelNameBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
var results []*dataloader.Result
|
||||
names := keys.Keys()
|
||||
|
||||
channels, err := channels.ListNames(names...)
|
||||
if err != nil {
|
||||
for range names {
|
||||
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
found := false
|
||||
for i := range channels {
|
||||
if channels[i].Name == name {
|
||||
results = append(results, &dataloader.Result{Data: channels[i]})
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
results = append(results, &dataloader.Result{Data: models.Channel{}, Error: err})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"git.aiterp.net/rpdata/api/models/characters"
|
||||
"github.com/graph-gophers/dataloader"
|
||||
)
|
||||
|
||||
// Character gets a character by key
|
||||
func (loader *Loader) Character(key, value string) (models.Character, error) {
|
||||
if !strings.HasPrefix(key, "Character.") {
|
||||
key = "Character." + key
|
||||
}
|
||||
|
||||
loader.loadPrimed(key)
|
||||
|
||||
if loader.loaders[key] == nil {
|
||||
return models.Character{}, errors.New("unsupported key")
|
||||
}
|
||||
|
||||
thunk := loader.loaders[key].Load(loader.ctx, dataloader.StringKey(value))
|
||||
res, err := thunk()
|
||||
if err != nil {
|
||||
return models.Character{}, err
|
||||
}
|
||||
|
||||
char, ok := res.(models.Character)
|
||||
if !ok {
|
||||
return models.Character{}, errors.New("incorrect type")
|
||||
}
|
||||
|
||||
return char, nil
|
||||
}
|
||||
|
||||
// Characters gets characters by key
|
||||
func (loader *Loader) Characters(key string, values ...string) ([]models.Character, error) {
|
||||
if !strings.HasPrefix(key, "Character.") {
|
||||
key = "Character." + key
|
||||
}
|
||||
|
||||
if loader.loaders[key] == nil {
|
||||
return nil, errors.New("unsupported key")
|
||||
}
|
||||
|
||||
loader.loadPrimed(key)
|
||||
|
||||
thunk := loader.loaders[key].LoadMany(loader.ctx, dataloader.NewKeysFromStrings(values))
|
||||
res, errs := thunk()
|
||||
for _, err := range errs {
|
||||
if err != nil && err != ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
chars := make([]models.Character, len(res))
|
||||
|
||||
for i := range res {
|
||||
char, ok := res[i].(models.Character)
|
||||
if !ok {
|
||||
return nil, errors.New("incorrect type")
|
||||
}
|
||||
|
||||
chars[i] = char
|
||||
}
|
||||
|
||||
return chars, nil
|
||||
}
|
||||
|
||||
// PrimeCharacters adds a set of characters to be loaded if, and only if, characters
|
||||
// are going to be loaded. This will fill up the cache and speed up subsequent dataloader
|
||||
// runs.
|
||||
func (loader *Loader) PrimeCharacters(key string, values ...string) {
|
||||
if !strings.HasPrefix(key, "Character.") {
|
||||
key = "Character." + key
|
||||
}
|
||||
|
||||
loader.prime(key, values)
|
||||
}
|
||||
|
||||
func characterIDBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
results := make([]*dataloader.Result, 0, len(keys))
|
||||
ids := keys.Keys()
|
||||
|
||||
characters, err := characters.List(&characters.Filter{IDs: ids})
|
||||
if err != nil {
|
||||
for range ids {
|
||||
results = append(results, &dataloader.Result{Error: err})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
found := false
|
||||
|
||||
for _, character := range characters {
|
||||
if character.ID == id {
|
||||
results = append(results, &dataloader.Result{Data: character})
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
results = append(results, &dataloader.Result{Data: models.Character{}, Error: ErrNotFound})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func characterNickBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
|
||||
var results []*dataloader.Result
|
||||
nicks := keys.Keys()
|
||||
|
||||
characters, err := characters.List(&characters.Filter{Nicks: nicks})
|
||||
if err != nil {
|
||||
for range nicks {
|
||||
results = append(results, &dataloader.Result{Error: err})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
for _, nick := range nicks {
|
||||
found := false
|
||||
for i := range characters {
|
||||
if characters[i].HasNick(nick) {
|
||||
results = append(results, &dataloader.Result{Data: characters[i]})
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
results = append(results, &dataloader.Result{Data: models.Character{}, Error: err})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/graph-gophers/dataloader"
|
||||
)
|
||||
|
||||
var contextKey struct{}
|
||||
|
||||
// ErrNotFound is returned in batches when one or more things weren't found. Usually harmless.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// A Loader is a collection of data loaders and functions to act on them. It's supposed to be
|
||||
// request-scoped, and will thus keep things cached indefinitely.
|
||||
type Loader struct {
|
||||
mutex sync.Mutex
|
||||
ctx context.Context
|
||||
loaders map[string]*dataloader.Loader
|
||||
|
||||
primedKeys map[string]map[string]bool
|
||||
}
|
||||
|
||||
// New initializes the loader.
|
||||
func New() *Loader {
|
||||
return &Loader{
|
||||
ctx: context.Background(),
|
||||
loaders: map[string]*dataloader.Loader{
|
||||
"Character.id": dataloader.NewBatchedLoader(characterIDBatch, dataloader.WithWait(time.Millisecond*2)),
|
||||
"Character.nick": dataloader.NewBatchedLoader(characterNickBatch, dataloader.WithWait(time.Millisecond*2)),
|
||||
"Channel.name": dataloader.NewBatchedLoader(channelNameBatch, dataloader.WithWait(time.Millisecond*2)),
|
||||
},
|
||||
primedKeys: make(map[string]map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// FromContext gets the Loader from context.
|
||||
func FromContext(ctx context.Context) *Loader {
|
||||
value := ctx.Value(&contextKey)
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return value.(*Loader)
|
||||
}
|
||||
|
||||
// ToContext gets a context with the loader as a value
|
||||
func (loader *Loader) ToContext(ctx context.Context) context.Context {
|
||||
loader.ctx = ctx
|
||||
return context.WithValue(ctx, &contextKey, loader)
|
||||
}
|
||||
|
||||
func (loader *Loader) prime(key string, values []string) {
|
||||
loader.mutex.Lock()
|
||||
if loader.primedKeys[key] == nil {
|
||||
loader.primedKeys[key] = make(map[string]bool)
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
loader.primedKeys[key][value] = true
|
||||
}
|
||||
loader.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (loader *Loader) loadPrimed(key string) {
|
||||
loader.mutex.Lock()
|
||||
if len(loader.primedKeys[key]) > 0 {
|
||||
primedKeys := make([]string, 0, len(loader.primedKeys[key]))
|
||||
for key := range loader.primedKeys[key] {
|
||||
primedKeys = append(primedKeys, key)
|
||||
}
|
||||
|
||||
loader.loaders[key].LoadMany(loader.ctx, dataloader.NewKeysFromStrings(primedKeys))
|
||||
loader.primedKeys[key] = nil
|
||||
}
|
||||
loader.mutex.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A Notifier is a synchronization primitive for waking upp all listeners at once.
|
||||
type Notifier struct {
|
||||
mutex sync.Mutex
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
// Broadcast wakes all listeners if there are any.
|
||||
func (notifier *Notifier) Broadcast() {
|
||||
notifier.mutex.Lock()
|
||||
if notifier.ch != nil {
|
||||
close(notifier.ch)
|
||||
notifier.ch = nil
|
||||
}
|
||||
notifier.mutex.Unlock()
|
||||
}
|
||||
|
||||
// C gets the channel that'll close on the next notification.
|
||||
func (notifier *Notifier) C() <-chan struct{} {
|
||||
notifier.mutex.Lock()
|
||||
if notifier.ch == nil {
|
||||
notifier.ch = make(chan struct{})
|
||||
}
|
||||
ch := notifier.ch
|
||||
notifier.mutex.Unlock()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// Wait waits for the next `Broadcast` call, or the context's termination.
|
||||
func (notifier *Notifier) Wait(ctx context.Context) error {
|
||||
select {
|
||||
case <-notifier.C():
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
var db *mgo.Database
|
||||
var dbInits []func(db *mgo.Database)
|
||||
|
||||
// ConnectDB connects to a mongodb database.
|
||||
func ConnectDB(host string, port int, database, username, password, mechanism string) error {
|
||||
session, err := mgo.DialWithInfo(&mgo.DialInfo{
|
||||
Addrs: []string{fmt.Sprintf("%s:%d", host, port)},
|
||||
Timeout: 30 * time.Second,
|
||||
Database: database,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Mechanism: mechanism,
|
||||
Source: database,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db = session.DB(database)
|
||||
|
||||
return setupDB()
|
||||
}
|
||||
|
||||
// HandleInit handles the initialization of the database
|
||||
func HandleInit(function func(db *mgo.Database)) {
|
||||
dbInits = append(dbInits, function)
|
||||
}
|
||||
|
||||
func setupDB() error {
|
||||
db.C("common.characters").EnsureIndexKey("name")
|
||||
db.C("common.characters").EnsureIndexKey("shortName")
|
||||
db.C("common.characters").EnsureIndexKey("author")
|
||||
err := db.C("common.characters").EnsureIndex(mgo.Index{
|
||||
Key: []string{"nicks"},
|
||||
Unique: true,
|
||||
DropDups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db.C("logbot3.logs").EnsureIndexKey("date")
|
||||
db.C("logbot3.logs").EnsureIndexKey("channel")
|
||||
db.C("logbot3.logs").EnsureIndexKey("channel", "open")
|
||||
db.C("logbot3.logs").EnsureIndexKey("open")
|
||||
db.C("logbot3.logs").EnsureIndexKey("oldId")
|
||||
db.C("logbot3.logs").EnsureIndexKey("characterIds")
|
||||
db.C("logbot3.logs").EnsureIndexKey("event")
|
||||
db.C("logbot3.logs").EnsureIndexKey("$text:channel", "$text:title", "$text:event", "$text:description", "$text:posts.nick", "$text:posts.text")
|
||||
|
||||
err = db.C("server.changes").EnsureIndex(mgo.Index{
|
||||
Key: []string{"date"},
|
||||
ExpireAfter: time.Hour * (24 * 14),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dbInit := range dbInits {
|
||||
dbInit(db)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,39 +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
|
||||
if sconf.Enabled {
|
||||
err = ConnectSpace(sconf.Host, sconf.AccessKey, sconf.SecretKey, sconf.Bucket, sconf.MaxSize, sconf.Root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hasInitialized = true
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,97 +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) {
|
||||
if spaceClient == nil {
|
||||
return "", errors.New("This functionality is not enabled")
|
||||
}
|
||||
|
||||
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 {
|
||||
if spaceClient == nil {
|
||||
return errors.New("This functionality is not enabled")
|
||||
}
|
||||
|
||||
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) {
|
||||
if spaceClient == nil {
|
||||
return nil, errors.New("This functionality is not enabled")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -26,6 +26,8 @@ const (
|
||||
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.
|
||||
|
||||
+161
-39
@@ -1,6 +1,9 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Change represents a change in the rpdata history through the API.
|
||||
type Change struct {
|
||||
@@ -12,14 +15,157 @@ type Change struct {
|
||||
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"`
|
||||
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.
|
||||
@@ -28,34 +174,10 @@ type ChangeKey struct {
|
||||
ID string `bson:"id"`
|
||||
}
|
||||
|
||||
// Objects makes a combined, mixed array of all the models stored in this change.
|
||||
func (change *Change) Objects() []interface{} {
|
||||
data := make([]interface{}, 0, len(change.Logs)+len(change.Characters)+len(change.Channels)+len(change.Posts)+len(change.Stories)+len(change.Tags)+len(change.Chapters)+len(change.Comments))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return data
|
||||
// ChangeFilter is a filter for listing changes.
|
||||
type ChangeFilter struct {
|
||||
Keys []ChangeKey
|
||||
EarliestDate *time.Time
|
||||
Author *string
|
||||
Limit *int
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package changekeys
|
||||
|
||||
import "git.aiterp.net/rpdata/api/models"
|
||||
|
||||
// Listed is a helper for cases like []models.ChangeKey{changekeys.All("Logs"), changekeys.One(log)}
|
||||
// Listed is a helper for cases like []models.ChangeKey{All("Log"), One(log), One(post)}
|
||||
func Listed(objects ...interface{}) []models.ChangeKey {
|
||||
keys := Many(objects)
|
||||
if len(keys) == 0 {
|
||||
|
||||
@@ -11,10 +11,16 @@ func One(object interface{}) models.ChangeKey {
|
||||
switch v := object.(type) {
|
||||
case models.Post:
|
||||
return models.ChangeKey{Model: "Post", ID: v.ID}
|
||||
case *models.Post:
|
||||
return models.ChangeKey{Model: "Post", ID: v.ID}
|
||||
case models.Character:
|
||||
return models.ChangeKey{Model: "Character", ID: v.ID}
|
||||
case *models.Character:
|
||||
return models.ChangeKey{Model: "Character", ID: v.ID}
|
||||
case models.Channel:
|
||||
return models.ChangeKey{Model: "Channel", ID: v.Name}
|
||||
case *models.Channel:
|
||||
return models.ChangeKey{Model: "Channel", ID: v.Name}
|
||||
}
|
||||
|
||||
model := ""
|
||||
@@ -26,6 +32,10 @@ func One(object interface{}) models.ChangeKey {
|
||||
|
||||
id := ""
|
||||
v := reflect.ValueOf(object)
|
||||
for v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if f := v.FieldByName("ID"); f.Kind() == reflect.String {
|
||||
id = f.String()
|
||||
} else if f = v.FieldByName("Name"); f.Kind() == reflect.String {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package changes
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var collection *mgo.Collection
|
||||
var submitMutex sync.Mutex
|
||||
|
||||
func list(query bson.M, limit int) ([]models.Change, error) {
|
||||
changes := make([]models.Change, 0, 64)
|
||||
err := collection.Find(query).Limit(limit).Sort("-date").All(&changes)
|
||||
|
||||
return changes, err
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
collection = db.C("common.changes")
|
||||
|
||||
collection.EnsureIndexKey("date")
|
||||
collection.EnsureIndexKey("author")
|
||||
collection.EnsureIndexKey("keys")
|
||||
|
||||
err := collection.EnsureIndex(mgo.Index{
|
||||
Name: "expiry",
|
||||
Key: []string{"date"},
|
||||
ExpireAfter: time.Hour * 2400, // 100 days
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package changes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
// Filter is a filter for changes.List.
|
||||
type Filter struct {
|
||||
Keys []models.ChangeKey
|
||||
EarliestDate *time.Time
|
||||
Limit *int
|
||||
}
|
||||
|
||||
// List lists changes.
|
||||
func List(filter *Filter) ([]models.Change, error) {
|
||||
query := bson.M{}
|
||||
limit := 0
|
||||
|
||||
if filter != nil {
|
||||
if filter.Limit != nil {
|
||||
limit = *filter.Limit
|
||||
}
|
||||
|
||||
if filter.Keys != nil {
|
||||
query["keys"] = bson.M{"$in": filter.Keys}
|
||||
} else {
|
||||
query["listed"] = true
|
||||
}
|
||||
|
||||
if filter.EarliestDate != nil {
|
||||
query["date"] = bson.M{"$gte": *filter.EarliestDate}
|
||||
}
|
||||
} else {
|
||||
query["listed"] = true
|
||||
limit = 256
|
||||
}
|
||||
|
||||
return list(query, limit)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package changes
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/counter"
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
// Submit a change to the database. The objects may be any supported model, or arrays.
|
||||
func Submit(model, op, author string, listed bool, keys []models.ChangeKey, objects ...interface{}) (models.Change, error) {
|
||||
submitMutex.Lock()
|
||||
defer submitMutex.Unlock()
|
||||
|
||||
id, err := counter.Next("auto_increment", "Change")
|
||||
if err != nil {
|
||||
return models.Change{}, err
|
||||
}
|
||||
|
||||
if !models.ChangeModel(model).IsValid() {
|
||||
panic("Invalid model")
|
||||
}
|
||||
|
||||
// Silently discard * keys on unlisted changes.
|
||||
if !listed {
|
||||
keysCopy := make([]models.ChangeKey, len(keys))
|
||||
copy(keysCopy, keys)
|
||||
keys = keysCopy
|
||||
|
||||
deletes := make([]int, 0, 1)
|
||||
for i, key := range keys {
|
||||
if key.ID == "*" {
|
||||
deletes = append(deletes, i-len(deletes))
|
||||
}
|
||||
}
|
||||
|
||||
for _, index := range deletes {
|
||||
keys = append(keys[:index], keys[index+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
change := models.Change{
|
||||
ID: "Change_" + strconv.Itoa(id),
|
||||
Model: models.ChangeModel(model),
|
||||
Date: time.Now(),
|
||||
Op: op,
|
||||
Author: author,
|
||||
Keys: keys,
|
||||
Listed: listed,
|
||||
}
|
||||
|
||||
for _, object := range objects {
|
||||
switch object := object.(type) {
|
||||
case models.Log:
|
||||
change.Logs = append(change.Logs, object)
|
||||
case []models.Log:
|
||||
change.Logs = append(change.Logs, object...)
|
||||
case models.Character:
|
||||
change.Characters = append(change.Characters, object)
|
||||
case []models.Character:
|
||||
change.Characters = append(change.Characters, object...)
|
||||
case models.Channel:
|
||||
change.Channels = append(change.Channels, object)
|
||||
case []models.Channel:
|
||||
change.Channels = append(change.Channels, object...)
|
||||
case models.Post:
|
||||
change.Posts = append(change.Posts, object)
|
||||
case []models.Post:
|
||||
change.Posts = append(change.Posts, object...)
|
||||
case models.Story:
|
||||
change.Stories = append(change.Stories, object)
|
||||
case []models.Story:
|
||||
change.Stories = append(change.Stories, object...)
|
||||
case models.Tag:
|
||||
change.Tags = append(change.Tags, object)
|
||||
case []models.Tag:
|
||||
change.Tags = append(change.Tags, object...)
|
||||
case models.Chapter:
|
||||
change.Chapters = append(change.Chapters, object)
|
||||
case []models.Chapter:
|
||||
change.Chapters = append(change.Chapters, object...)
|
||||
case models.Comment:
|
||||
change.Comments = append(change.Comments, object)
|
||||
case []models.Comment:
|
||||
change.Comments = append(change.Comments, object...)
|
||||
default:
|
||||
log.Printf("Warning: unrecognized object in change: %#+v", object)
|
||||
}
|
||||
}
|
||||
|
||||
pushToSubscribers(change)
|
||||
|
||||
err = collection.Insert(&change)
|
||||
if err != nil {
|
||||
return models.Change{}, err
|
||||
}
|
||||
|
||||
return change, nil
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package changes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"git.aiterp.net/rpdata/api/models"
|
||||
)
|
||||
|
||||
var subMutex sync.Mutex
|
||||
var subList []*subscription
|
||||
|
||||
type subscription struct {
|
||||
Keys map[string]bool
|
||||
Channel chan<- *models.Change
|
||||
WildCard bool
|
||||
}
|
||||
|
||||
// Subscribe subscribes to all changes.
|
||||
func Subscribe(ctx context.Context, keys []models.ChangeKey, wildcard bool) <-chan *models.Change {
|
||||
channel := make(chan *models.Change, 64)
|
||||
sub := &subscription{
|
||||
Keys: make(map[string]bool, len(keys)),
|
||||
Channel: channel,
|
||||
WildCard: wildcard,
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
sub.Keys[mapKey(key)] = true
|
||||
}
|
||||
|
||||
subMutex.Lock()
|
||||
subList = append(subList, sub)
|
||||
subMutex.Unlock()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
|
||||
subMutex.Lock()
|
||||
for i := range subList {
|
||||
if subList[i] == sub {
|
||||
subList = append(subList[:i], subList[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
subMutex.Unlock()
|
||||
}()
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
func pushToSubscribers(change models.Change) {
|
||||
keys := make([]string, len(change.Keys))
|
||||
for i := range change.Keys {
|
||||
keys[i] = mapKey(change.Keys[i])
|
||||
}
|
||||
|
||||
subMutex.Lock()
|
||||
SubLoop:
|
||||
for _, sub := range subList {
|
||||
changeCopy := change
|
||||
|
||||
if sub.WildCard && change.Listed {
|
||||
select {
|
||||
case sub.Channel <- &changeCopy:
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
for _, key := range keys {
|
||||
if sub.Keys[key] {
|
||||
select {
|
||||
case sub.Channel <- &changeCopy:
|
||||
default:
|
||||
}
|
||||
|
||||
continue SubLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
subMutex.Unlock()
|
||||
}
|
||||
|
||||
func mapKey(ck models.ChangeKey) string {
|
||||
return ck.Model.String() + "." + ck.ID
|
||||
}
|
||||
|
||||
func init() {
|
||||
subList = make([]*subscription, 0, 16)
|
||||
}
|
||||
+18
-1
@@ -13,4 +13,21 @@ type Channel struct {
|
||||
// ChangeObject in GQL.
|
||||
func (*Channel) IsChangeObject() {
|
||||
panic("this method is a dummy, and so is its caller")
|
||||
}
|
||||
}
|
||||
|
||||
// ChannelFilter is a filter for channel listing.
|
||||
type ChannelFilter struct {
|
||||
Names []string `json:"names"`
|
||||
Logged *bool `json:"logged"`
|
||||
EventName *string `json:"eventName"`
|
||||
LocationName *string `json:"locationName"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// ChannelUpdate is a filter for channel listing.
|
||||
type ChannelUpdate struct {
|
||||
Logged *bool `json:"logged"`
|
||||
Hub *bool `json:"hub"`
|
||||
EventName *string `json:"eventName"`
|
||||
LocationName *string `json:"locationName"`
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user