This commit is contained in:
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="gisle">
|
||||||
|
<words>
|
||||||
|
<w>logbot</w>
|
||||||
|
<w>mirc</w>
|
||||||
|
<w>rctx</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
@@ -10,12 +10,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ErrDriverUnrecognized is returned if the driver is not recognized
|
// ErrDriverUnrecognized is returned if the driver is not recognized
|
||||||
var ErrDriverUnrecognized = errors.New("Driver not recognized, check your config or update rpdata")
|
var ErrDriverUnrecognized = errors.New("driver not recognized, check installed version or your configuration")
|
||||||
|
|
||||||
type Database interface {
|
type Database interface {
|
||||||
Changes() repositories.ChangeRepository
|
Changes() repositories.ChangeRepository
|
||||||
Characters() repositories.CharacterRepository
|
Characters() repositories.CharacterRepository
|
||||||
Tags() repositories.TagRepository
|
Tags() repositories.TagRepository
|
||||||
|
Logs() repositories.LogRepository
|
||||||
|
Posts() repositories.PostRepository
|
||||||
|
|
||||||
Close(ctx context.Context) error
|
Close(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func (r *characterRepository) List(ctx context.Context, filter models.CharacterF
|
|||||||
}
|
}
|
||||||
|
|
||||||
characters := make([]*models.Character, 0, 32)
|
characters := make([]*models.Character, 0, 32)
|
||||||
err := r.characters.Find(query).All(&characters)
|
err := r.characters.Find(query).Limit(filter.Limit).All(&characters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == mgo.ErrNotFound {
|
if err == mgo.ErrNotFound {
|
||||||
return characters, nil
|
return characters, nil
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type MongoDB struct {
|
|||||||
changes repositories.ChangeRepository
|
changes repositories.ChangeRepository
|
||||||
characters repositories.CharacterRepository
|
characters repositories.CharacterRepository
|
||||||
tags repositories.TagRepository
|
tags repositories.TagRepository
|
||||||
|
logs *logRepository
|
||||||
|
posts *postRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MongoDB) Changes() repositories.ChangeRepository {
|
func (m *MongoDB) Changes() repositories.ChangeRepository {
|
||||||
@@ -31,6 +33,14 @@ func (m *MongoDB) Tags() repositories.TagRepository {
|
|||||||
return m.tags
|
return m.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Logs() repositories.LogRepository {
|
||||||
|
return m.logs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MongoDB) Posts() repositories.PostRepository {
|
||||||
|
return m.posts
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MongoDB) Close(ctx context.Context) error {
|
func (m *MongoDB) Close(ctx context.Context) error {
|
||||||
m.session.Close()
|
m.session.Close()
|
||||||
return nil
|
return nil
|
||||||
@@ -70,12 +80,28 @@ func Init(cfg config.Database) (*MongoDB, error) {
|
|||||||
return nil, err
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
go posts.fixPositions(logs)
|
||||||
|
|
||||||
return &MongoDB{
|
return &MongoDB{
|
||||||
session: session,
|
session: session,
|
||||||
|
|
||||||
changes: changes,
|
changes: changes,
|
||||||
characters: characters,
|
characters: characters,
|
||||||
tags: newTagRepository(db),
|
tags: newTagRepository(db),
|
||||||
|
logs: logs,
|
||||||
|
posts: posts,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
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{"$in": 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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[1:] {
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/channels"
|
"git.aiterp.net/rpdata/api/models/channels"
|
||||||
"git.aiterp.net/rpdata/api/models/files"
|
"git.aiterp.net/rpdata/api/models/files"
|
||||||
"git.aiterp.net/rpdata/api/models/logs"
|
|
||||||
"git.aiterp.net/rpdata/api/models/posts"
|
|
||||||
"git.aiterp.net/rpdata/api/models/stories"
|
"git.aiterp.net/rpdata/api/models/stories"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +31,7 @@ func complexity() (cr graphcore.ComplexityRoot) {
|
|||||||
cr.Query.Post = func(childComplexity int, id string) int {
|
cr.Query.Post = func(childComplexity int, id string) int {
|
||||||
return childComplexity + findComplexity
|
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
|
return childComplexity + listComplexity
|
||||||
}
|
}
|
||||||
cr.Query.UnknownNicks = func(childComplexity int, filter *graphcore.UnknownNicksFilter) int {
|
cr.Query.UnknownNicks = func(childComplexity int, filter *graphcore.UnknownNicksFilter) int {
|
||||||
@@ -42,7 +40,7 @@ func complexity() (cr graphcore.ComplexityRoot) {
|
|||||||
cr.Query.Log = func(childComplexity int, id string) int {
|
cr.Query.Log = func(childComplexity int, id string) int {
|
||||||
return childComplexity + findComplexity
|
return childComplexity + findComplexity
|
||||||
}
|
}
|
||||||
cr.Query.Logs = func(childComplexity int, filter *logs.Filter) int {
|
cr.Query.Logs = func(childComplexity int, filter *models.LogFilter) int {
|
||||||
if filter != nil && filter.Open != nil && *filter.Open == true {
|
if filter != nil && filter.Open != nil && *filter.Open == true {
|
||||||
return childComplexity + findComplexity
|
return childComplexity + findComplexity
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -30,11 +30,11 @@ models:
|
|||||||
Post:
|
Post:
|
||||||
model: git.aiterp.net/rpdata/api/models.Post
|
model: git.aiterp.net/rpdata/api/models.Post
|
||||||
PostsFilter:
|
PostsFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/posts.Filter
|
model: git.aiterp.net/rpdata/api/models.PostFilter
|
||||||
Log:
|
Log:
|
||||||
model: git.aiterp.net/rpdata/api/models.Log
|
model: git.aiterp.net/rpdata/api/models.Log
|
||||||
LogsFilter:
|
LogsFilter:
|
||||||
model: git.aiterp.net/rpdata/api/models/logs.Filter
|
model: git.aiterp.net/rpdata/api/models.LogFilter
|
||||||
LogImporter:
|
LogImporter:
|
||||||
model: git.aiterp.net/rpdata/api/models.LogImporter
|
model: git.aiterp.net/rpdata/api/models.LogImporter
|
||||||
Comment:
|
Comment:
|
||||||
|
|||||||
+12
-131
@@ -3,74 +3,25 @@ package resolvers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
"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"
|
||||||
"git.aiterp.net/rpdata/api/models/changekeys"
|
|
||||||
"git.aiterp.net/rpdata/api/models/changes"
|
|
||||||
"git.aiterp.net/rpdata/api/models/channels"
|
|
||||||
"git.aiterp.net/rpdata/api/models/logs"
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
|
|
||||||
func (r *queryResolver) Log(ctx context.Context, id string) (*models.Log, error) {
|
func (r *queryResolver) Log(ctx context.Context, id string) (*models.Log, error) {
|
||||||
log, err := logs.FindID(id)
|
return r.s.Logs.Find(ctx, id)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &log, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) Logs(ctx context.Context, filter *logs.Filter) ([]*models.Log, error) {
|
func (r *queryResolver) Logs(ctx context.Context, filter *models.LogFilter) ([]*models.Log, error) {
|
||||||
logs, err := logs.List(filter)
|
return r.s.Logs.List(ctx, 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
|
// Mutations
|
||||||
|
|
||||||
func (r *mutationResolver) AddLog(ctx context.Context, input graphcore.LogAddInput) (*models.Log, error) {
|
func (r *mutationResolver) AddLog(ctx context.Context, input graphcore.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
|
open := input.Open != nil && *input.Open == true
|
||||||
title := ""
|
title := ""
|
||||||
if input.Title != nil {
|
if input.Title != nil {
|
||||||
@@ -85,33 +36,10 @@ func (r *mutationResolver) AddLog(ctx context.Context, input graphcore.LogAddInp
|
|||||||
description = *input.Description
|
description = *input.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
existingChannel, _ := channels.FindName(input.Channel)
|
return r.s.Logs.Create(ctx, title, description, input.Channel, event, open)
|
||||||
|
|
||||||
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 graphcore.LogImportInput) ([]*models.Log, error) {
|
func (r *mutationResolver) ImportLog(ctx context.Context, input graphcore.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{}
|
date := time.Time{}
|
||||||
if input.Date != nil {
|
if input.Date != nil {
|
||||||
date = *input.Date
|
date = *input.Date
|
||||||
@@ -127,67 +55,20 @@ func (r *mutationResolver) ImportLog(ctx context.Context, input graphcore.LogImp
|
|||||||
tz = parsedTZ
|
tz = parsedTZ
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := logs.Import(input.Importer, date, tz, input.ChannelName, input.Data)
|
return r.s.Logs.Import(ctx, 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 graphcore.LogEditInput) (*models.Log, error) {
|
func (r *mutationResolver) EditLog(ctx context.Context, input graphcore.LogEditInput) (*models.Log, error) {
|
||||||
token := auth.TokenFromContext(ctx)
|
update := models.LogUpdate{
|
||||||
if !token.Authenticated() || !token.Permitted("log.edit") {
|
Open: input.Open,
|
||||||
return nil, errors.New("You are not permitted to edit logs")
|
Description: input.Description,
|
||||||
|
EventName: input.Event,
|
||||||
|
Title: input.Title,
|
||||||
}
|
}
|
||||||
|
|
||||||
log, err := logs.FindID(input.ID)
|
return r.s.Logs.Update(ctx, input.ID, update)
|
||||||
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 graphcore.LogRemoveInput) (*models.Log, error) {
|
func (r *mutationResolver) RemoveLog(ctx context.Context, input graphcore.LogRemoveInput) (*models.Log, error) {
|
||||||
token := auth.TokenFromContext(ctx)
|
return r.s.Logs.Delete(ctx, input.ID)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-153
@@ -2,180 +2,39 @@ package resolvers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
"git.aiterp.net/rpdata/api/graph2/graphcore"
|
||||||
"git.aiterp.net/rpdata/api/internal/auth"
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"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"
|
|
||||||
"git.aiterp.net/rpdata/api/models/posts"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
|
|
||||||
func (r *queryResolver) Post(ctx context.Context, id string) (*models.Post, error) {
|
func (r *queryResolver) Post(ctx context.Context, id string) (*models.Post, error) {
|
||||||
post, err := posts.FindID(id)
|
return r.s.Logs.FindPosts(ctx, id)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &post, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) Posts(ctx context.Context, filter *posts.Filter) ([]*models.Post, error) {
|
func (r *queryResolver) Posts(ctx context.Context, filter *models.PostFilter) ([]*models.Post, error) {
|
||||||
// Some sanity checks to avoid querying an insame amount of posts.
|
return r.s.Logs.ListPosts(ctx, filter)
|
||||||
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
|
// Mutation
|
||||||
|
|
||||||
func (r *mutationResolver) AddPost(ctx context.Context, input graphcore.PostAddInput) (*models.Post, error) {
|
func (r *mutationResolver) AddPost(ctx context.Context, input graphcore.PostAddInput) (*models.Post, error) {
|
||||||
token := auth.TokenFromContext(ctx)
|
return r.s.Logs.AddPost(ctx, input.LogID, input.Time, input.Kind, input.Nick, input.Text)
|
||||||
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 graphcore.PostEditInput) (*models.Post, error) {
|
func (r *mutationResolver) EditPost(ctx context.Context, input graphcore.PostEditInput) (*models.Post, error) {
|
||||||
token := auth.TokenFromContext(ctx)
|
return r.s.Logs.EditPost(ctx, input.ID, models.PostUpdate{
|
||||||
if !token.Authenticated() || !token.Permitted("post.edit") {
|
Time: input.Time,
|
||||||
return nil, errors.New("You are not permitted to edit logs")
|
Kind: input.Kind,
|
||||||
}
|
Nick: input.Nick,
|
||||||
|
Text: input.Text,
|
||||||
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 graphcore.PostMoveInput) ([]*models.Post, error) {
|
func (r *mutationResolver) MovePost(ctx context.Context, input graphcore.PostMoveInput) ([]*models.Post, error) {
|
||||||
token := auth.TokenFromContext(ctx)
|
return r.s.Logs.MovePost(ctx, input.ID, input.ToPosition)
|
||||||
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 graphcore.PostRemoveInput) (*models.Post, error) {
|
func (r *mutationResolver) RemovePost(ctx context.Context, input graphcore.PostRemoveInput) (*models.Post, error) {
|
||||||
token := auth.TokenFromContext(ctx)
|
return r.s.Logs.DeletePost(ctx, input.ID)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ input PostRemoveInput {
|
|||||||
|
|
||||||
# Filter for posts query
|
# Filter for posts query
|
||||||
input PostsFilter {
|
input PostsFilter {
|
||||||
id: [String!]
|
ids: [String!]
|
||||||
kind: [String!]
|
kinds: [String!]
|
||||||
logId: String
|
logId: String
|
||||||
limit: Int
|
limit: Int
|
||||||
}
|
}
|
||||||
+3
-13
@@ -7,10 +7,10 @@ import (
|
|||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/loader"
|
"git.aiterp.net/rpdata/api/internal/loader"
|
||||||
"git.aiterp.net/rpdata/api/models"
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"git.aiterp.net/rpdata/api/models/posts"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type logResolver struct {
|
type logResolver struct {
|
||||||
|
logs *services.LogService
|
||||||
characters *services.CharacterService
|
characters *services.CharacterService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,20 +34,10 @@ func (r *logResolver) Characters(ctx context.Context, log *models.Log) ([]*model
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *logResolver) Posts(ctx context.Context, log *models.Log, kinds []string) ([]*models.Post, error) {
|
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})
|
return r.logs.ListPosts(ctx, &models.PostFilter{LogID: &log.ShortID})
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
posts2 := make([]*models.Post, len(posts))
|
|
||||||
for i := range posts {
|
|
||||||
posts2[i] = &posts[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return posts2, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogResolver is a resolver
|
// LogResolver is a resolver
|
||||||
func LogResolver(s *services.Bundle) *logResolver {
|
func LogResolver(s *services.Bundle) *logResolver {
|
||||||
return &logResolver{characters: s.Characters}
|
return &logResolver{characters: s.Characters, logs: s.Logs}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
+26
-26
@@ -15,54 +15,54 @@ type Change struct {
|
|||||||
Keys []ChangeKey `bson:"keys"`
|
Keys []ChangeKey `bson:"keys"`
|
||||||
Date time.Time `bson:"date"`
|
Date time.Time `bson:"date"`
|
||||||
|
|
||||||
Logs []Log `bson:"logs"`
|
Logs []*Log `bson:"logs"`
|
||||||
Characters []Character `bson:"characters"`
|
Characters []*Character `bson:"characters"`
|
||||||
Channels []Channel `bson:"channels"`
|
Channels []*Channel `bson:"channels"`
|
||||||
Posts []Post `bson:"posts"`
|
Posts []*Post `bson:"posts"`
|
||||||
Stories []Story `bson:"stories"`
|
Stories []*Story `bson:"stories"`
|
||||||
Tags []Tag `bson:"tags"`
|
Tags []*Tag `bson:"tags"`
|
||||||
Chapters []Chapter `bson:"chapters"`
|
Chapters []*Chapter `bson:"chapters"`
|
||||||
Comments []Comment `bson:"comments"`
|
Comments []*Comment `bson:"comments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddObject adds the model into the appropriate array.
|
// AddObject adds the model into the appropriate array.
|
||||||
func (change *Change) AddObject(object interface{}) bool {
|
func (change *Change) AddObject(object interface{}) bool {
|
||||||
if v := reflect.ValueOf(object); v.Kind() == reflect.Ptr {
|
if v := reflect.ValueOf(object); v.Kind() != reflect.Ptr && v.Kind() != reflect.Slice {
|
||||||
return change.AddObject(v.Elem().Interface())
|
return change.AddObject(v.Addr().Interface())
|
||||||
}
|
}
|
||||||
|
|
||||||
switch object := object.(type) {
|
switch object := object.(type) {
|
||||||
case Log:
|
case *Log:
|
||||||
change.Logs = append(change.Logs, object)
|
change.Logs = append(change.Logs, object)
|
||||||
case []Log:
|
case []*Log:
|
||||||
change.Logs = append(change.Logs, object...)
|
change.Logs = append(change.Logs, object...)
|
||||||
case Character:
|
case *Character:
|
||||||
change.Characters = append(change.Characters, object)
|
change.Characters = append(change.Characters, object)
|
||||||
case []Character:
|
case []*Character:
|
||||||
change.Characters = append(change.Characters, object...)
|
change.Characters = append(change.Characters, object...)
|
||||||
case Channel:
|
case *Channel:
|
||||||
change.Channels = append(change.Channels, object)
|
change.Channels = append(change.Channels, object)
|
||||||
case []Channel:
|
case []*Channel:
|
||||||
change.Channels = append(change.Channels, object...)
|
change.Channels = append(change.Channels, object...)
|
||||||
case Post:
|
case *Post:
|
||||||
change.Posts = append(change.Posts, object)
|
change.Posts = append(change.Posts, object)
|
||||||
case []Post:
|
case []*Post:
|
||||||
change.Posts = append(change.Posts, object...)
|
change.Posts = append(change.Posts, object...)
|
||||||
case Story:
|
case *Story:
|
||||||
change.Stories = append(change.Stories, object)
|
change.Stories = append(change.Stories, object)
|
||||||
case []Story:
|
case []*Story:
|
||||||
change.Stories = append(change.Stories, object...)
|
change.Stories = append(change.Stories, object...)
|
||||||
case Tag:
|
case *Tag:
|
||||||
change.Tags = append(change.Tags, object)
|
change.Tags = append(change.Tags, object)
|
||||||
case []Tag:
|
case []*Tag:
|
||||||
change.Tags = append(change.Tags, object...)
|
change.Tags = append(change.Tags, object...)
|
||||||
case Chapter:
|
case *Chapter:
|
||||||
change.Chapters = append(change.Chapters, object)
|
change.Chapters = append(change.Chapters, object)
|
||||||
case []Chapter:
|
case []*Chapter:
|
||||||
change.Chapters = append(change.Chapters, object...)
|
change.Chapters = append(change.Chapters, object...)
|
||||||
case Comment:
|
case *Comment:
|
||||||
change.Comments = append(change.Comments, object)
|
change.Comments = append(change.Comments, object)
|
||||||
case []Comment:
|
case []*Comment:
|
||||||
change.Comments = append(change.Comments, object...)
|
change.Comments = append(change.Comments, object...)
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package changekeys
|
|||||||
|
|
||||||
import "git.aiterp.net/rpdata/api/models"
|
import "git.aiterp.net/rpdata/api/models"
|
||||||
|
|
||||||
// Listed is a helper for cases like []models.ChangeKey{changekeys.All("Logs"), changekeys.One(log)}
|
// Listed is a helper for cases like []models.ChangeKey{All("Log"), One(log), One(post)}
|
||||||
func Listed(objects ...interface{}) []models.ChangeKey {
|
func Listed(objects ...interface{}) []models.ChangeKey {
|
||||||
keys := Many(objects)
|
keys := Many(objects)
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ func One(object interface{}) models.ChangeKey {
|
|||||||
switch v := object.(type) {
|
switch v := object.(type) {
|
||||||
case models.Post:
|
case models.Post:
|
||||||
return models.ChangeKey{Model: "Post", ID: v.ID}
|
return models.ChangeKey{Model: "Post", ID: v.ID}
|
||||||
|
case *models.Post:
|
||||||
|
return models.ChangeKey{Model: "Post", ID: v.ID}
|
||||||
case models.Character:
|
case models.Character:
|
||||||
return models.ChangeKey{Model: "Character", ID: v.ID}
|
return models.ChangeKey{Model: "Character", ID: v.ID}
|
||||||
|
case *models.Character:
|
||||||
|
return models.ChangeKey{Model: "Character", ID: v.ID}
|
||||||
case models.Channel:
|
case models.Channel:
|
||||||
return models.ChangeKey{Model: "Channel", ID: v.Name}
|
return models.ChangeKey{Model: "Channel", ID: v.Name}
|
||||||
|
case *models.Channel:
|
||||||
|
return models.ChangeKey{Model: "Channel", ID: v.Name}
|
||||||
}
|
}
|
||||||
|
|
||||||
model := ""
|
model := ""
|
||||||
|
|||||||
+35
-27
@@ -54,53 +54,61 @@ func Submit(model, op, author string, listed bool, keys []models.ChangeKey, obje
|
|||||||
for _, object := range objects {
|
for _, object := range objects {
|
||||||
switch object := object.(type) {
|
switch object := object.(type) {
|
||||||
case models.Log:
|
case models.Log:
|
||||||
change.Logs = append(change.Logs, object)
|
change.Logs = append(change.Logs, &object)
|
||||||
case *models.Log:
|
case *models.Log:
|
||||||
change.Logs = append(change.Logs, *object)
|
change.Logs = append(change.Logs, object)
|
||||||
case []models.Log:
|
case []models.Log:
|
||||||
change.Logs = append(change.Logs, object...)
|
for _, obj := range object {
|
||||||
|
change.Logs = append(change.Logs, &obj)
|
||||||
|
}
|
||||||
case models.Character:
|
case models.Character:
|
||||||
change.Characters = append(change.Characters, object)
|
change.Characters = append(change.Characters, &object)
|
||||||
case *models.Character:
|
case *models.Character:
|
||||||
change.Characters = append(change.Characters, *object)
|
change.Characters = append(change.Characters, object)
|
||||||
case []models.Character:
|
case []models.Character:
|
||||||
change.Characters = append(change.Characters, object...)
|
for _, obj := range object {
|
||||||
|
change.Characters = append(change.Characters, &obj)
|
||||||
|
}
|
||||||
case models.Channel:
|
case models.Channel:
|
||||||
change.Channels = append(change.Channels, object)
|
change.Channels = append(change.Channels, &object)
|
||||||
case *models.Channel:
|
case *models.Channel:
|
||||||
change.Channels = append(change.Channels, *object)
|
change.Channels = append(change.Channels, object)
|
||||||
case []models.Channel:
|
case []models.Channel:
|
||||||
change.Channels = append(change.Channels, object...)
|
for _, obj := range object {
|
||||||
|
change.Channels = append(change.Channels, &obj)
|
||||||
|
}
|
||||||
case models.Post:
|
case models.Post:
|
||||||
change.Posts = append(change.Posts, object)
|
change.Posts = append(change.Posts, &object)
|
||||||
case *models.Post:
|
case *models.Post:
|
||||||
change.Posts = append(change.Posts, *object)
|
change.Posts = append(change.Posts, object)
|
||||||
case []models.Post:
|
case []models.Post:
|
||||||
change.Posts = append(change.Posts, object...)
|
for _, obj := range object {
|
||||||
|
change.Posts = append(change.Posts, &obj)
|
||||||
|
}
|
||||||
case models.Story:
|
case models.Story:
|
||||||
change.Stories = append(change.Stories, object)
|
change.Stories = append(change.Stories, &object)
|
||||||
case *models.Story:
|
case *models.Story:
|
||||||
change.Stories = append(change.Stories, *object)
|
change.Stories = append(change.Stories, object)
|
||||||
case []models.Story:
|
case []models.Story:
|
||||||
change.Stories = append(change.Stories, object...)
|
for _, obj := range object {
|
||||||
case models.Tag:
|
change.Stories = append(change.Stories, &obj)
|
||||||
change.Tags = append(change.Tags, object)
|
}
|
||||||
case *models.Tag:
|
|
||||||
change.Tags = append(change.Tags, *object)
|
|
||||||
case []models.Tag:
|
|
||||||
change.Tags = append(change.Tags, object...)
|
|
||||||
case models.Chapter:
|
case models.Chapter:
|
||||||
change.Chapters = append(change.Chapters, object)
|
change.Chapters = append(change.Chapters, &object)
|
||||||
case *models.Chapter:
|
case *models.Chapter:
|
||||||
change.Chapters = append(change.Chapters, *object)
|
change.Chapters = append(change.Chapters, object)
|
||||||
case []models.Chapter:
|
case []models.Chapter:
|
||||||
change.Chapters = append(change.Chapters, object...)
|
for _, obj := range object {
|
||||||
|
change.Chapters = append(change.Chapters, &obj)
|
||||||
|
}
|
||||||
case models.Comment:
|
case models.Comment:
|
||||||
change.Comments = append(change.Comments, object)
|
change.Comments = append(change.Comments, &object)
|
||||||
case *models.Comment:
|
case *models.Comment:
|
||||||
change.Comments = append(change.Comments, *object)
|
change.Comments = append(change.Comments, object)
|
||||||
case []models.Comment:
|
case []models.Comment:
|
||||||
change.Comments = append(change.Comments, object...)
|
for _, obj := range object {
|
||||||
|
change.Comments = append(change.Comments, &obj)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
log.Printf("Warning: unrecognized object in change: %#+v", object)
|
log.Printf("Warning: unrecognized object in change: %#+v", object)
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -19,4 +19,21 @@ type Log struct {
|
|||||||
// ChangeObject in GQL.
|
// ChangeObject in GQL.
|
||||||
func (*Log) IsChangeObject() {
|
func (*Log) IsChangeObject() {
|
||||||
panic("this method is a dummy, and so is its caller")
|
panic("this method is a dummy, and so is its caller")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A LogFilter is a filter that can be used to list logs.
|
||||||
|
type LogFilter struct {
|
||||||
|
Search *string
|
||||||
|
Open *bool
|
||||||
|
Characters []string
|
||||||
|
Channels []string
|
||||||
|
Events []string
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogUpdate struct {
|
||||||
|
Title *string
|
||||||
|
EventName *string
|
||||||
|
Description *string
|
||||||
|
Open *bool
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package logs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/store"
|
"git.aiterp.net/rpdata/api/internal/store"
|
||||||
@@ -45,21 +44,5 @@ func init() {
|
|||||||
store.HandleInit(func(db *mgo.Database) {
|
store.HandleInit(func(db *mgo.Database) {
|
||||||
collection = db.C("logbot3.logs")
|
collection = db.C("logbot3.logs")
|
||||||
postCollection = db.C("logbot3.posts")
|
postCollection = db.C("logbot3.posts")
|
||||||
|
|
||||||
collection.EnsureIndexKey("date")
|
|
||||||
collection.EnsureIndexKey("channel")
|
|
||||||
collection.EnsureIndexKey("characterIds")
|
|
||||||
collection.EnsureIndexKey("event")
|
|
||||||
collection.EnsureIndex(mgo.Index{
|
|
||||||
Key: []string{"channel", "open"},
|
|
||||||
})
|
|
||||||
err := collection.EnsureIndex(mgo.Index{
|
|
||||||
Key: []string{"shortId"},
|
|
||||||
Unique: true,
|
|
||||||
DropDups: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("init logbot3.logs:", err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
package logs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/importers/mirclike"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/posts"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/channels"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/importers/forumlog"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An ImportedLog contains data about an imported log.
|
|
||||||
type ImportedLog struct {
|
|
||||||
Log models.Log
|
|
||||||
Posts []models.Post
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import makes a log and posts object from different formats.
|
|
||||||
func Import(importer models.LogImporter, date time.Time, tz *time.Location, channelName string, data string) ([]ImportedLog, error) {
|
|
||||||
results := make([]ImportedLog, 0, 8)
|
|
||||||
|
|
||||||
eventName := ""
|
|
||||||
if channel, err := channels.FindName(channelName); err != nil {
|
|
||||||
eventName = channel.EventName
|
|
||||||
}
|
|
||||||
|
|
||||||
date = date.In(tz)
|
|
||||||
|
|
||||||
switch importer {
|
|
||||||
case models.LogImporterMircLike:
|
|
||||||
{
|
|
||||||
if date.IsZero() {
|
|
||||||
return nil, errors.New("Date is not optional for mirc-like logs")
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedLog, parsedPosts, err := mirclike.ParseLog(data, date, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log, err := Add(parsedLog.Date, channelName, "", eventName, "", false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
posts, err := posts.AddMany(log, parsedPosts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, ImportedLog{
|
|
||||||
Log: log,
|
|
||||||
Posts: posts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
case models.LogImporterForumLog:
|
|
||||||
{
|
|
||||||
parseResults, err := forumlog.ParseLogs(data, tz)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, result := range parseResults {
|
|
||||||
log, err := Add(result.Log.Date, channelName, "", eventName, "", false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
posts, err := posts.AddMany(log, result.Posts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, ImportedLog{
|
|
||||||
Log: log,
|
|
||||||
Posts: posts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
return nil, errors.New("Invalid importer: " + importer.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
+17
-1
@@ -17,4 +17,20 @@ type Post struct {
|
|||||||
// ChangeObject in GQL.
|
// ChangeObject in GQL.
|
||||||
func (*Post) IsChangeObject() {
|
func (*Post) IsChangeObject() {
|
||||||
panic("this method is a dummy, and so is its caller")
|
panic("this method is a dummy, and so is its caller")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostFilter is used to generate a query to the database.
|
||||||
|
type PostFilter struct {
|
||||||
|
IDs []string
|
||||||
|
Kinds []string
|
||||||
|
LogID *string
|
||||||
|
Search *string
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostUpdate struct {
|
||||||
|
Time *time.Time
|
||||||
|
Kind *string
|
||||||
|
Nick *string
|
||||||
|
Text *string
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*models.Log, error)
|
||||||
|
List(ctx context.Context, filter models.LogFilter) ([]*models.Log, error)
|
||||||
|
Insert(ctx context.Context, log models.Log) (*models.Log, error)
|
||||||
|
Update(ctx context.Context, log models.Log, update models.LogUpdate) (*models.Log, error)
|
||||||
|
Delete(ctx context.Context, log models.Log) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*models.Post, error)
|
||||||
|
List(ctx context.Context, filter models.PostFilter) ([]*models.Post, error)
|
||||||
|
Insert(ctx context.Context, post models.Post) (*models.Post, error)
|
||||||
|
InsertMany(ctx context.Context, posts ...*models.Post) ([]*models.Post, error)
|
||||||
|
Update(ctx context.Context, post models.Post, update models.PostUpdate) (*models.Post, error)
|
||||||
|
Move(ctx context.Context, post models.Post, position int) ([]*models.Post, error)
|
||||||
|
Delete(ctx context.Context, post models.Post) error
|
||||||
|
}
|
||||||
@@ -3,4 +3,10 @@ package repositories
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
// ErrNotFound should be returned instead of any database-specific not found error.
|
// ErrNotFound should be returned instead of any database-specific not found error.
|
||||||
var ErrNotFound = errors.New("Resource not found")
|
var ErrNotFound = errors.New("resource not found")
|
||||||
|
|
||||||
|
// ErrInvalidPosition is returned when the log post is attempted moved outside of the range.
|
||||||
|
var ErrInvalidPosition = errors.New("position is out of bounds")
|
||||||
|
|
||||||
|
// ErrParentMismatch is returned if the list refer to different parent objects when it shouldn't.
|
||||||
|
var ErrParentMismatch = errors.New("parent resource is not the same for the entire list")
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ func (s *CharacterService) AddNick(ctx context.Context, id string, nick string)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.loader.Clear(character.ID)
|
||||||
|
s.loader.Prime(character.ID, character)
|
||||||
|
|
||||||
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
@@ -171,6 +174,9 @@ func (s *CharacterService) RemoveNick(ctx context.Context, id string, nick strin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.loader.Clear(character.ID)
|
||||||
|
s.loader.Prime(character.ID, character)
|
||||||
|
|
||||||
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
@@ -192,6 +198,9 @@ func (s *CharacterService) Delete(ctx context.Context, id string) (*models.Chara
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.loader.Clear(character.ID)
|
||||||
|
s.loader.Prime(character.ID, character)
|
||||||
|
|
||||||
s.changeService.Submit(ctx, "Character", "remove", true, changekeys.Listed(character), character)
|
s.changeService.Submit(ctx, "Character", "remove", true, changekeys.Listed(character), character)
|
||||||
|
|
||||||
return character, nil
|
return character, nil
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"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/channels"
|
||||||
|
"git.aiterp.net/rpdata/api/repositories"
|
||||||
|
"git.aiterp.net/rpdata/api/services/parsers"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogService struct {
|
||||||
|
logs repositories.LogRepository
|
||||||
|
posts repositories.PostRepository
|
||||||
|
changeService *ChangeService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) Find(ctx context.Context, id string) (*models.Log, error) {
|
||||||
|
return s.logs.Find(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) FindPosts(ctx context.Context, id string) (*models.Post, error) {
|
||||||
|
return s.posts.Find(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) List(ctx context.Context, filter *models.LogFilter) ([]*models.Log, error) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &models.LogFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.logs.List(ctx, *filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) ListPosts(ctx context.Context, filter *models.PostFilter) ([]*models.Post, error) {
|
||||||
|
// Some sanity checks to avoid querying an insame amount of posts.
|
||||||
|
if filter == nil {
|
||||||
|
filter = &models.PostFilter{Limit: 100}
|
||||||
|
} else {
|
||||||
|
if (filter.Limit <= 0 || filter.Limit > 512) && (filter.LogID == nil && len(filter.IDs) == 0) {
|
||||||
|
return nil, errors.New("a limit of 0 (no limit) or >512 without a logId or a set of IDs is not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.IDs) > 100 {
|
||||||
|
return nil, errors.New("you may not query for more than 100 ids, split your query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.posts.List(ctx, *filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) Create(ctx context.Context, title, description, channelName, eventName string, open bool) (*models.Log, error) {
|
||||||
|
if channelName == "" {
|
||||||
|
return nil, errors.New("channel name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := &models.Log{
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
ChannelName: channelName,
|
||||||
|
EventName: eventName,
|
||||||
|
Date: time.Now(),
|
||||||
|
Open: open,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := auth.CheckPermission(ctx, "add", log); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: s.channelService.Ensure()
|
||||||
|
|
||||||
|
log, err := s.logs.Insert(ctx, *log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
|
||||||
|
|
||||||
|
return log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import creates new logs from common formats.
|
||||||
|
func (s *LogService) Import(ctx context.Context, importer models.LogImporter, date time.Time, tz *time.Location, channelName string, data string) ([]*models.Log, error) {
|
||||||
|
if err := auth.CheckPermission(ctx, "add", &models.Log{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]*models.Log, 0, 8)
|
||||||
|
|
||||||
|
eventName := ""
|
||||||
|
if channel, err := channels.FindName(channelName); err != nil {
|
||||||
|
eventName = channel.EventName
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Ensure channel
|
||||||
|
|
||||||
|
date = date.In(tz)
|
||||||
|
|
||||||
|
switch importer {
|
||||||
|
case models.LogImporterMircLike:
|
||||||
|
{
|
||||||
|
if date.IsZero() {
|
||||||
|
return nil, errors.New("date is not optional for mirc-like logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := parsers.MircLog(data, date, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsed.Log.EventName = eventName
|
||||||
|
parsed.Log.ChannelName = channelName
|
||||||
|
|
||||||
|
log, err := s.logs.Insert(ctx, parsed.Log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, post := range parsed.Posts {
|
||||||
|
post.LogID = log.ShortID
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
|
||||||
|
|
||||||
|
results = append(results, log)
|
||||||
|
}
|
||||||
|
case models.LogImporterForumLog:
|
||||||
|
{
|
||||||
|
parseResults, err := parsers.ForumLog(data, tz)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, parsed := range parseResults {
|
||||||
|
log, err := s.logs.Insert(ctx, parsed.Log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsed.Log.EventName = eventName
|
||||||
|
parsed.Log.ChannelName = channelName
|
||||||
|
|
||||||
|
for _, post := range parsed.Posts {
|
||||||
|
post.LogID = log.ShortID
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := s.posts.InsertMany(ctx, parsed.Posts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelLog, "add", true, changekeys.Listed(log), log)
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Listed(log, posts), log, posts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
return nil, errors.New("Invalid importer: " + importer.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) Update(ctx context.Context, id string, update models.LogUpdate) (*models.Log, error) {
|
||||||
|
log, err := s.logs.Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := auth.CheckPermission(ctx, "edit", log); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log, err = s.logs.Update(ctx, *log, update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelLog, "edit", true, changekeys.Listed(log), log)
|
||||||
|
|
||||||
|
return log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) AddPost(ctx context.Context, logId string, time time.Time, kind, nick, text string) (*models.Post, error) {
|
||||||
|
if kind == "" || nick == "" || time.IsZero() {
|
||||||
|
return nil, errors.New("kind, nick and time must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
log, err := s.logs.Find(ctx, logId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
post := &models.Post{
|
||||||
|
LogID: log.ShortID,
|
||||||
|
Kind: kind,
|
||||||
|
Nick: nick,
|
||||||
|
Text: text,
|
||||||
|
Time: time,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := auth.CheckPermission(ctx, "add", post); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err = s.posts.Insert(ctx, *post)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelPost, "add", true, changekeys.Many(log, post), log, post)
|
||||||
|
|
||||||
|
return post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) EditPost(ctx context.Context, id string, update models.PostUpdate) (*models.Post, error) {
|
||||||
|
if (update.Kind != nil && *update.Kind == "") || (update.Nick != nil && *update.Nick == "") || (update.Text != nil && *update.Text == "") {
|
||||||
|
return nil, errors.New("kind, nick and time must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := s.posts.Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := auth.CheckPermission(ctx, "edit", post); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err = s.posts.Update(ctx, *post, update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log, err := s.logs.Find(context.Background(), post.LogID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelPost, "edit", true, changekeys.Many(log, post), post)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) MovePost(ctx context.Context, id string, position int) ([]*models.Post, error) {
|
||||||
|
if position < 1 {
|
||||||
|
return nil, repositories.ErrInvalidPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := s.posts.Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := auth.CheckPermission(ctx, "move", post); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := s.posts.Move(ctx, *post, position)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if len(posts) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log, err := s.logs.Find(context.Background(), posts[0].LogID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelPost, "move", true, changekeys.Many(log, posts), posts)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) DeletePost(ctx context.Context, id string) (*models.Post, error) {
|
||||||
|
post, err := s.posts.Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := auth.CheckPermission(ctx, "remove", post); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.posts.Delete(ctx, *post)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log, err := s.logs.Find(context.Background(), post.LogID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelPost, "remove", true, changekeys.Many(log, post), post)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LogService) Delete(ctx context.Context, id string) (*models.Log, error) {
|
||||||
|
log, err := s.logs.Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := auth.CheckPermission(ctx, "remove", log); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.logs.Delete(ctx, *log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.changeService.Submit(ctx, models.ChangeModelLog, "remove", true, changekeys.Listed(log), log)
|
||||||
|
|
||||||
|
return log, nil
|
||||||
|
}
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
package forumlog
|
package parsers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/importers/mirclike"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// A ParsedLog contains the parsed log header and its posts.
|
// A ParsedLog contains the parsed log header and its posts.
|
||||||
type ParsedLog struct {
|
type ParsedLog struct {
|
||||||
Log models.Log
|
Log models.Log
|
||||||
Posts []models.Post
|
Posts []*models.Post
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLogs parses the logs from the data.
|
// ForumLog parses the logs from the data.
|
||||||
func ParseLogs(data string, tz *time.Location) ([]ParsedLog, error) {
|
func ForumLog(data string, tz *time.Location) ([]ParsedLog, error) {
|
||||||
metadata := ParseMetadata(data)
|
metadata := ForumLogMetadata(data)
|
||||||
results := make([]ParsedLog, 0, len(metadata["Date"]))
|
results := make([]ParsedLog, 0, len(metadata["Date"]))
|
||||||
scanner := bufio.NewScanner(strings.NewReader(data))
|
scanner := bufio.NewScanner(strings.NewReader(data))
|
||||||
|
|
||||||
@@ -27,11 +24,11 @@ func ParseLogs(data string, tz *time.Location) ([]ParsedLog, error) {
|
|||||||
// Parse date
|
// Parse date
|
||||||
date, err := time.ParseInLocation("January 2, 2006", dateStr, tz)
|
date, err := time.ParseInLocation("January 2, 2006", dateStr, tz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to parse date #%d: %#+v is not the in the correct format of \"January 2, 2006\"", i+1, dateStr)
|
return nil, fmt.Errorf("failed to parse date #%d: %#+v is not the in the correct format of \"January 2, 2006\"", i+1, dateStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse posts
|
// Parse posts
|
||||||
posts := make([]models.Post, 0, 128)
|
posts := make([]*models.Post, 0, 128)
|
||||||
parsing := false
|
parsing := false
|
||||||
prev := ""
|
prev := ""
|
||||||
prevPost := models.Post{}
|
prevPost := models.Post{}
|
||||||
@@ -66,7 +63,7 @@ func ParseLogs(data string, tz *time.Location) ([]ParsedLog, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse the post.
|
// Parse the post.
|
||||||
post, err := mirclike.ParsePost(line, date, prevPost)
|
post, err := MircPost(line, date, prevPost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
summary := ""
|
summary := ""
|
||||||
for _, ru := range line {
|
for _, ru := range line {
|
||||||
@@ -77,17 +74,17 @@ func ParseLogs(data string, tz *time.Location) ([]ParsedLog, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("Failed to parse post: %s", summary)
|
return nil, fmt.Errorf("failed to parse post: %s", summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
posts = append(posts, post)
|
posts = append(posts, &post)
|
||||||
prevPost = post
|
prevPost = post
|
||||||
prev = line
|
prev = line
|
||||||
}
|
}
|
||||||
|
|
||||||
// No posts means there's a problem.
|
// No posts means there's a problem.
|
||||||
if len(posts) == 0 {
|
if len(posts) == 0 {
|
||||||
return nil, fmt.Errorf("Session %d (%s) has no posts (too many dates?)", i+1, dateStr)
|
return nil, fmt.Errorf("session %d (%s) has no posts (too many dates?)", i+1, dateStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add it.
|
// Add it.
|
||||||
@@ -99,3 +96,32 @@ func ParseLogs(data string, tz *time.Location) ([]ParsedLog, error) {
|
|||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForumLogMetadata 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 ForumLogMetadata(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,31 +1,71 @@
|
|||||||
package forumlog_test
|
package parsers_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/services/parsers"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/internal/importers/forumlog"
|
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseLogs(t *testing.T) {
|
func TestForumLogMetadata(t *testing.T) {
|
||||||
results, err := forumlog.ParseLogs(testLog, time.UTC)
|
data := parsers.ForumLogMetadata(testData1)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"August 11, 2014",
|
||||||
|
"August 12, 2014",
|
||||||
|
"August 13, 2014",
|
||||||
|
}, data["Date"])
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"Aroste, an island off the Derraian coastline.",
|
||||||
|
}, data["Location"])
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"Gisle",
|
||||||
|
}, data["GM"])
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"847 posts",
|
||||||
|
}, data["Length"])
|
||||||
|
assert.Equal(t, len(data["Stuff"]), 0)
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"Calyx Vadris - Osiris",
|
||||||
|
"Damien Monroe - Bowe",
|
||||||
|
"Jason Wolfe - Dante",
|
||||||
|
"Renala T'Iavay - Gisle",
|
||||||
|
"Victoria Steels - MCB280",
|
||||||
|
}, data["Characters"])
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"Recurring:",
|
||||||
|
"Halisi - Tyranniac",
|
||||||
|
"Jattic - Dante",
|
||||||
|
"Jelvan Darennon - Gisle",
|
||||||
|
"Leah - Dante",
|
||||||
|
"Marissa T'Evin - Gisle",
|
||||||
|
"Philip Lacour - Tyranniac",
|
||||||
|
}, data["NPCs"], "Labels immediately following another should be considered part of the former's content.")
|
||||||
|
|
||||||
|
for key := range data {
|
||||||
|
if strings.HasPrefix(key, "[") {
|
||||||
|
t.Error("It should have stopped at the IRC post, yet this key exists:", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForumLog(t *testing.T) {
|
||||||
|
results, err := parsers.ForumLog(testLog2, time.UTC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Parse: %s", err)
|
t.Fatalf("Parse: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 2, len(results), "Amount of results.")
|
assert.Equal(t, 2, len(results), "Amount of results.")
|
||||||
assert.Equal(t, testLogPosts[0], results[0].Posts, "First log's posts.")
|
assert.Equal(t, testLog2Posts[0], results[0].Posts, "First log's posts.")
|
||||||
assert.Equal(t, testLogPosts[1], results[1].Posts, "Second log's posts.")
|
assert.Equal(t, testLog2Posts[1], results[1].Posts, "Second log's posts.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLogsErrors(t *testing.T) {
|
func TestForumLogErrors(t *testing.T) {
|
||||||
_, err1 := forumlog.ParseLogs(brokenLogNoPosts, time.UTC)
|
_, err1 := parsers.ForumLog(brokenLogNoPosts, time.UTC)
|
||||||
_, err2 := forumlog.ParseLogs(brokenLogBrokenPost, time.UTC)
|
_, err2 := parsers.ForumLog(brokenLogBrokenPost, time.UTC)
|
||||||
_, err3 := forumlog.ParseLogs(brokenLogBrokenDate, time.UTC)
|
_, err3 := parsers.ForumLog(brokenLogBrokenDate, time.UTC)
|
||||||
|
|
||||||
t.Log("Should be about no posts:", err1)
|
t.Log("Should be about no posts:", err1)
|
||||||
t.Log("Should be about a broken post:", err2)
|
t.Log("Should be about a broken post:", err2)
|
||||||
@@ -36,44 +76,7 @@ func TestParseLogsErrors(t *testing.T) {
|
|||||||
assert.NotEqual(t, err3, nil, "Should be about a broken date")
|
assert.NotEqual(t, err3, nil, "Should be about a broken date")
|
||||||
}
|
}
|
||||||
|
|
||||||
var testLog = `
|
var testLog2Posts = [][]*models.Post{
|
||||||
Date:
|
|
||||||
July 25, 2013
|
|
||||||
July 26, 2013
|
|
||||||
|
|
||||||
GM:
|
|
||||||
Tyranniac
|
|
||||||
|
|
||||||
Length:
|
|
||||||
92 posts
|
|
||||||
|
|
||||||
Characters:
|
|
||||||
Fera'Sel nar Veltar - Baphomet
|
|
||||||
Renala T’Iavay - Gisle
|
|
||||||
Steven Briggs - Dante
|
|
||||||
|
|
||||||
NPCs:
|
|
||||||
Receptionist
|
|
||||||
|
|
||||||
Relevant RPs:
|
|
||||||
<-- Hub - Miner's Respite (July 16)
|
|
||||||
--> Event - Hinpinn's Salvage Mission (August 5)
|
|
||||||
|
|
||||||
[21:28] * @Tyranniac | The Hnipinn Minerals local administrative center is a rather small building located next to the refinery. The area is mostly asphalt and industrial surroundings. The gate in the fence surrounding the refinery is nearby, with a transport truck just being admitted by the guard - rather heavily armed for corporate security. Despite not being that late, it's rather dark due to the rain-bearing clouds that have just started emptying their content. The windows of the office glow with invitingly warm light. A receptionist can be seen working through the glass door.
|
|
||||||
|
|
||||||
[21:46] * Steve_Briggs approaches the building at a brisk jog, pulling up the collar of his jacket to block as much of the rain as possible. Stopping just short of the door, he holds back and awaits Renala. "Well, hopefully this is the universe's way of getting our bad luck out of the way early." he says, glancing up at the sky. "Good things to come!" he adds with a smile.
|
|
||||||
|
|
||||||
|
|
||||||
[21:46] * Steve_Briggs approaches the building at a brisk jog, pulling up the collar of his jacket to block as much of the rain as possible. Stopping just short of the door, he holds back and awaits Renala. "Well, hopefully this is the universe's way of getting our bad luck out of the way early." he says, glancing up at the sky. "Good things to come!" he adds with a smile.
|
|
||||||
|
|
||||||
--> Stuff and things
|
|
||||||
|
|
||||||
<-- Stuff and things
|
|
||||||
|
|
||||||
[21:46] * Steve_Briggs approaches the building at a brisk jog, pulling up the collar of his jacket to block as much of the rain as possible. Stopping just short of the door, he holds back and awaits Renala. "Well, hopefully this is the universe's way of getting our bad luck out of the way early." he says, glancing up at the sky. "Good things to come!" he adds with a smile.
|
|
||||||
`
|
|
||||||
|
|
||||||
var testLogPosts = [][]models.Post{
|
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
ID: "UNASSIGNED",
|
ID: "UNASSIGNED",
|
||||||
@@ -116,6 +119,51 @@ var testLogPosts = [][]models.Post{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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?"
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
var brokenLogNoPosts = `
|
var brokenLogNoPosts = `
|
||||||
Date:
|
Date:
|
||||||
September 27, 2014
|
September 27, 2014
|
||||||
@@ -137,15 +185,39 @@ Date:
|
|||||||
[12:34] * =Scene= Stuff happens.
|
[12:34] * =Scene= Stuff happens.
|
||||||
`
|
`
|
||||||
|
|
||||||
func parseDate(t *testing.T, date string) time.Time {
|
var testLog2 = `
|
||||||
result, err := time.Parse("2006-01-02 15:04:05", date)
|
Date:
|
||||||
if err != nil {
|
July 25, 2013
|
||||||
if t != nil {
|
July 26, 2013
|
||||||
t.Fatal("Could not parse date", date, err)
|
|
||||||
} else {
|
|
||||||
panic("Could not parse date: " + err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
GM:
|
||||||
}
|
Tyranniac
|
||||||
|
|
||||||
|
Length:
|
||||||
|
92 posts
|
||||||
|
|
||||||
|
Characters:
|
||||||
|
Fera'Sel nar Veltar - Baphomet
|
||||||
|
Renala T’Iavay - Gisle
|
||||||
|
Steven Briggs - Dante
|
||||||
|
|
||||||
|
NPCs:
|
||||||
|
Receptionist
|
||||||
|
|
||||||
|
Relevant RPs:
|
||||||
|
<-- Hub - Miner's Respite (July 16)
|
||||||
|
--> Event - Hinpinn's Salvage Mission (August 5)
|
||||||
|
|
||||||
|
[21:28] * @Tyranniac | The Hnipinn Minerals local administrative center is a rather small building located next to the refinery. The area is mostly asphalt and industrial surroundings. The gate in the fence surrounding the refinery is nearby, with a transport truck just being admitted by the guard - rather heavily armed for corporate security. Despite not being that late, it's rather dark due to the rain-bearing clouds that have just started emptying their content. The windows of the office glow with invitingly warm light. A receptionist can be seen working through the glass door.
|
||||||
|
|
||||||
|
[21:46] * Steve_Briggs approaches the building at a brisk jog, pulling up the collar of his jacket to block as much of the rain as possible. Stopping just short of the door, he holds back and awaits Renala. "Well, hopefully this is the universe's way of getting our bad luck out of the way early." he says, glancing up at the sky. "Good things to come!" he adds with a smile.
|
||||||
|
|
||||||
|
|
||||||
|
[21:46] * Steve_Briggs approaches the building at a brisk jog, pulling up the collar of his jacket to block as much of the rain as possible. Stopping just short of the door, he holds back and awaits Renala. "Well, hopefully this is the universe's way of getting our bad luck out of the way early." he says, glancing up at the sky. "Good things to come!" he adds with a smile.
|
||||||
|
|
||||||
|
--> Stuff and things
|
||||||
|
|
||||||
|
<-- Stuff and things
|
||||||
|
|
||||||
|
[21:46] * Steve_Briggs approaches the building at a brisk jog, pulling up the collar of his jacket to block as much of the rain as possible. Stopping just short of the door, he holds back and awaits Renala. "Well, hopefully this is the universe's way of getting our bad luck out of the way early." he says, glancing up at the sky. "Good things to come!" he adds with a smile.
|
||||||
|
`
|
||||||
@@ -1,20 +1,58 @@
|
|||||||
package mirclike
|
package parsers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNotPost is returned by parsePost if the line is empty or not a post.
|
// ErrNotPost is returned by parsePost if the line is empty or not a post.
|
||||||
var ErrNotPost = errors.New("not a post")
|
var ErrNotPost = errors.New("not a post")
|
||||||
|
|
||||||
// ParsePost parses a post from a mirc-like line. If the previous post is included (it can be empty), it will be used
|
// ErrEmptyLog is returned by ParseLog if there are no (valid) posts in the log.
|
||||||
|
var ErrEmptyLog = errors.New("no valid posts found in log")
|
||||||
|
|
||||||
|
// MircLog parses the log and returns the things that can be gleamed from them.
|
||||||
|
func MircLog(data string, date time.Time, strict bool) (*ParsedLog, 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 := MircPost(line, date, prev)
|
||||||
|
if err != nil {
|
||||||
|
if strict {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
posts = append(posts, &post)
|
||||||
|
prev = post
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(posts) == 0 {
|
||||||
|
return nil, ErrEmptyLog
|
||||||
|
}
|
||||||
|
|
||||||
|
log := models.Log{
|
||||||
|
Date: posts[0].Time,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ParsedLog{log, posts}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MircPost parses a post from a mirc-like line. If the previous post is included (it can be empty), it will be used
|
||||||
// to determine whether midnight has passed.
|
// to determine whether midnight has passed.
|
||||||
func ParsePost(line string, date time.Time, prev models.Post) (models.Post, error) {
|
func MircPost(line string, date time.Time, prev models.Post) (models.Post, error) {
|
||||||
// Do basic validation
|
// Do basic validation
|
||||||
line = strings.Trim(line, " \t\n\r")
|
line = strings.Trim(line, " \t\n\r")
|
||||||
if len(line) == 0 || !strings.HasPrefix(line, "[") {
|
if len(line) == 0 || !strings.HasPrefix(line, "[") {
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package parsers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
"git.aiterp.net/rpdata/api/services/parsers"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMircPost(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 := parsers.MircPost(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 TestMircPostErrors(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
Input string
|
||||||
|
Err error
|
||||||
|
}{
|
||||||
|
{"[12:34] <Stuff> Things said.", nil},
|
||||||
|
{"[12:34] >Stuff> Things said.", parsers.ErrNotPost},
|
||||||
|
{"12:34] <Stuff> Things said.", parsers.ErrNotPost},
|
||||||
|
{"* Stuff Things said.", parsers.ErrNotPost},
|
||||||
|
{"", parsers.ErrNotPost},
|
||||||
|
{"[12:34 <Stuff> Things said.", parsers.ErrNotPost},
|
||||||
|
{"[TE:XT] <Stuff> Things said.", parsers.ErrNotPost},
|
||||||
|
{"[10] <Stuff> Things said.", parsers.ErrNotPost},
|
||||||
|
{"[12:34:56:789] <Stuff> Things said.", nil},
|
||||||
|
{"[12:34:56.789] <Stuff> Things said.", parsers.ErrNotPost},
|
||||||
|
{"[12:34] <Stuff>", parsers.ErrNotPost},
|
||||||
|
{"[12:34] * Stuff", parsers.ErrNotPost},
|
||||||
|
{"[12:34] =Scene=", parsers.ErrNotPost},
|
||||||
|
{"[12:34] <=Scene=>", parsers.ErrNotPost},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, row := range table {
|
||||||
|
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) {
|
||||||
|
_, err := parsers.MircPost(row.Input, time.Now(), models.Post{})
|
||||||
|
|
||||||
|
assert.Equal(t, row.Err, err, "Error should match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMircPostNextDay(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 := parsers.MircPost(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 TestMircLog(t *testing.T) {
|
||||||
|
parsed, err := parsers.MircLog(testLog, parseDate(t, "2018-05-11 00:00:00"), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("ParseLog", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, testLogPosts, parsed.Posts)
|
||||||
|
assert.Equal(t, parsed.Posts[0].Time, parsed.Log.Date, "Log's date should be the first post's.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMircLogErrors(t *testing.T) {
|
||||||
|
_, err1 := parsers.MircLog("\n\n\n\n\n \n\t\r\n", time.Time{}, false)
|
||||||
|
_, err2 := parsers.MircLog("\n\n\n\n\n[14:57]* Stuff \n\t\r\n", time.Time{}, true)
|
||||||
|
|
||||||
|
assert.Equal(t, parsers.ErrEmptyLog, err1)
|
||||||
|
assert.Equal(t, parsers.ErrNotPost, err2)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -10,12 +10,12 @@ type Bundle struct {
|
|||||||
Tags *TagService
|
Tags *TagService
|
||||||
Characters *CharacterService
|
Characters *CharacterService
|
||||||
Changes *ChangeService
|
Changes *ChangeService
|
||||||
|
Logs *LogService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBundle creates a new bundle.
|
// NewBundle creates a new bundle.
|
||||||
func NewBundle(db database.Database) *Bundle {
|
func NewBundle(db database.Database) *Bundle {
|
||||||
bundle := &Bundle{}
|
bundle := &Bundle{}
|
||||||
|
|
||||||
bundle.Changes = &ChangeService{
|
bundle.Changes = &ChangeService{
|
||||||
changes: db.Changes(),
|
changes: db.Changes(),
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,11 @@ func NewBundle(db database.Database) *Bundle {
|
|||||||
loader: loaders.CharacterLoaderFromRepository(db.Characters()),
|
loader: loaders.CharacterLoaderFromRepository(db.Characters()),
|
||||||
changeService: bundle.Changes,
|
changeService: bundle.Changes,
|
||||||
}
|
}
|
||||||
|
bundle.Logs = &LogService{
|
||||||
|
logs: db.Logs(),
|
||||||
|
posts: db.Posts(),
|
||||||
|
changeService: bundle.Changes,
|
||||||
|
}
|
||||||
|
|
||||||
return bundle
|
return bundle
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user