graph2: Added importLog query and associated models and repository functions.
the build was successful
the build was successful
This commit is contained in:
@@ -31,6 +31,8 @@ models:
|
|||||||
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/logs.Filter
|
||||||
|
LogImporter:
|
||||||
|
model: git.aiterp.net/rpdata/api/models.LogImporter
|
||||||
Chapter:
|
Chapter:
|
||||||
model: git.aiterp.net/rpdata/api/models.Chapter
|
model: git.aiterp.net/rpdata/api/models.Chapter
|
||||||
fields:
|
fields:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.aiterp.net/rpdata/api/models/channels"
|
"git.aiterp.net/rpdata/api/models/channels"
|
||||||
|
|
||||||
@@ -96,6 +97,50 @@ func (r *mutationResolver) AddLog(ctx context.Context, input input.LogAddInput)
|
|||||||
return log, nil
|
return log, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) ImportLog(ctx context.Context, input input.LogImportInput) ([]models.Log, error) {
|
||||||
|
token := auth.TokenFromContext(ctx)
|
||||||
|
if !token.Authenticated() || !token.Permitted("log.add") {
|
||||||
|
return nil, errors.New("You are not permitted to add logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
date := time.Time{}
|
||||||
|
if input.Date != nil {
|
||||||
|
date = *input.Date
|
||||||
|
}
|
||||||
|
|
||||||
|
tz := time.UTC
|
||||||
|
if input.Timezone != nil {
|
||||||
|
parsedTZ, err := time.LoadLocation(*input.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Unknown timezone: " + *input.Timezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
tz = parsedTZ
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := logs.Import(input.Importer, date, tz, input.ChannelName, input.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newLogs := make([]models.Log, 0, len(results))
|
||||||
|
for _, result := range results {
|
||||||
|
go func() {
|
||||||
|
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)
|
||||||
|
}()
|
||||||
|
|
||||||
|
log, err := logs.UpdateCharacters(result.Log)
|
||||||
|
if err != nil {
|
||||||
|
log = result.Log
|
||||||
|
}
|
||||||
|
|
||||||
|
newLogs = append(newLogs, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newLogs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) EditLog(ctx context.Context, input input.LogEditInput) (models.Log, error) {
|
func (r *mutationResolver) EditLog(ctx context.Context, input input.LogEditInput) (models.Log, error) {
|
||||||
token := auth.TokenFromContext(ctx)
|
token := auth.TokenFromContext(ctx)
|
||||||
if !token.Authenticated() || !token.Permitted("log.edit") {
|
if !token.Authenticated() || !token.Permitted("log.edit") {
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ type Mutation {
|
|||||||
# Add a new log
|
# Add a new log
|
||||||
addLog(input: LogAddInput!): Log!
|
addLog(input: LogAddInput!): Log!
|
||||||
|
|
||||||
|
# Import a log
|
||||||
|
importLog(input: LogImportInput!): [Log!]!
|
||||||
|
|
||||||
# Edit a log
|
# Edit a log
|
||||||
editLog(input: LogEditInput!): Log!
|
editLog(input: LogEditInput!): Log!
|
||||||
|
|
||||||
|
|||||||
@@ -101,3 +101,35 @@ input LogRemoveInput {
|
|||||||
# The id of the log to remove
|
# The id of the log to remove
|
||||||
id: String!
|
id: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for importLog mutation.
|
||||||
|
"""
|
||||||
|
input LogImportInput {
|
||||||
|
"The channel of the log, alwyas required."
|
||||||
|
channelName: String!
|
||||||
|
|
||||||
|
"Which importer to use."
|
||||||
|
importer: LogImporter!
|
||||||
|
|
||||||
|
"The timezone of the import, all log and post dates will be treated as it. It will be UTC if none is specified."
|
||||||
|
timezone: String
|
||||||
|
|
||||||
|
"The date of the log, if not provided in the log body."
|
||||||
|
date: Date
|
||||||
|
|
||||||
|
"The log body itself."
|
||||||
|
data: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LogImporter {
|
||||||
|
"""
|
||||||
|
mIRC-like: This importer parses logs that copied from mIRC posts without spaces.
|
||||||
|
"""
|
||||||
|
MircLike
|
||||||
|
|
||||||
|
"""
|
||||||
|
Forum log: This importer parses the format on the forum. The displayed log, not the post source.
|
||||||
|
"""
|
||||||
|
ForumLog
|
||||||
|
}
|
||||||
@@ -30,6 +30,24 @@ func Next(category, name string) (int, error) {
|
|||||||
return doc.Value, nil
|
return doc.Value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NextMany gets the next value of a counter, or an error if it hasn't, and increments by a specified value.
|
||||||
|
// Any value `returned` to `returned+(increment-1)` should then be safe to use.
|
||||||
|
func NextMany(category, name string, increment int) (int, error) {
|
||||||
|
id := category + "." + name
|
||||||
|
doc := counter{}
|
||||||
|
|
||||||
|
_, err := collection.Find(bson.M{"_id": id}).Apply(mgo.Change{
|
||||||
|
Update: bson.M{"$inc": bson.M{"value": 1}},
|
||||||
|
Upsert: true,
|
||||||
|
ReturnNew: true,
|
||||||
|
}, &doc)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
store.HandleInit(func(db *mgo.Database) {
|
store.HandleInit(func(db *mgo.Database) {
|
||||||
collection = db.C("core.counters")
|
collection = db.C("core.counters")
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ type ParsedLog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseLogs parses the logs from the data.
|
// ParseLogs parses the logs from the data.
|
||||||
func ParseLogs(data string) ([]ParsedLog, error) {
|
func ParseLogs(data string, tz *time.Location) ([]ParsedLog, error) {
|
||||||
metadata := ParseMetadata(data)
|
metadata := ParseMetadata(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))
|
||||||
|
|
||||||
for i, dateStr := range metadata["Date"] {
|
for i, dateStr := range metadata["Date"] {
|
||||||
// Parse date
|
// Parse date
|
||||||
date, err := time.Parse("January 2, 2006", dateStr)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestParseLogs(t *testing.T) {
|
func TestParseLogs(t *testing.T) {
|
||||||
results, err := forumlog.ParseLogs(testLog)
|
results, err := forumlog.ParseLogs(testLog, time.UTC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Parse: %s", err)
|
t.Fatalf("Parse: %s", err)
|
||||||
}
|
}
|
||||||
@@ -23,9 +23,9 @@ func TestParseLogs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLogsErrors(t *testing.T) {
|
func TestParseLogsErrors(t *testing.T) {
|
||||||
_, err1 := forumlog.ParseLogs(brokenLogNoPosts)
|
_, err1 := forumlog.ParseLogs(brokenLogNoPosts, time.UTC)
|
||||||
_, err2 := forumlog.ParseLogs(brokenLogBrokenPost)
|
_, err2 := forumlog.ParseLogs(brokenLogBrokenPost, time.UTC)
|
||||||
_, err3 := forumlog.ParseLogs(brokenLogBrokenDate)
|
_, err3 := forumlog.ParseLogs(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)
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogImporter describes a model related log importing.
|
||||||
|
type LogImporter string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LogImporterMircLike is a value of LogImporter
|
||||||
|
LogImporterMircLike LogImporter = "MircLike"
|
||||||
|
// LogImporterForumLog is a value of LogImporter
|
||||||
|
LogImporterForumLog LogImporter = "ForumLog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValid returns true if the underlying string is one of the correct values.
|
||||||
|
func (e LogImporter) IsValid() bool {
|
||||||
|
switch e {
|
||||||
|
case LogImporterForumLog, LogImporterMircLike:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e LogImporter) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalGQL unmarshals the underlying graphql value.
|
||||||
|
func (e *LogImporter) UnmarshalGQL(v interface{}) error {
|
||||||
|
str, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enums must be strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = LogImporter(str)
|
||||||
|
if !e.IsValid() {
|
||||||
|
return fmt.Errorf("%s is not a valid LogImporter", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalGQL marshals the underlying graphql value.
|
||||||
|
func (e LogImporter) MarshalGQL(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package posts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.aiterp.net/rpdata/api/internal/counter"
|
||||||
|
"git.aiterp.net/rpdata/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddMany adds multiple posts in on query. Each post gets a new ID and is associated with the log.
|
||||||
|
func AddMany(log models.Log, posts []models.Post) ([]models.Post, error) {
|
||||||
|
docs := make([]interface{}, len(posts))
|
||||||
|
|
||||||
|
mutex.RLock()
|
||||||
|
defer mutex.RUnlock()
|
||||||
|
|
||||||
|
startPosition, err := counter.NextMany("next_post_id", log.ShortID, len(posts))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, post := range posts {
|
||||||
|
post.ID = generateID(post.Time)
|
||||||
|
post.LogID = log.ShortID
|
||||||
|
post.Position = startPosition + i
|
||||||
|
|
||||||
|
docs[i] = post
|
||||||
|
}
|
||||||
|
|
||||||
|
err = collection.Insert(docs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user