diff --git a/graph2/gqlgen.yml b/graph2/gqlgen.yml index 543b08d..e0ba07d 100644 --- a/graph2/gqlgen.yml +++ b/graph2/gqlgen.yml @@ -35,6 +35,10 @@ models: model: git.aiterp.net/rpdata/api/models/logs.Filter LogImporter: model: git.aiterp.net/rpdata/api/models.LogImporter + Comment: + model: git.aiterp.net/rpdata/api/models.Comment + ChapterCommentMode: + model: git.aiterp.net/rpdata/api/models.ChapterCommentMode Chapter: model: git.aiterp.net/rpdata/api/models.Chapter fields: diff --git a/graph2/graph.go b/graph2/graph.go index 28d0a1c..607a6a0 100644 --- a/graph2/graph.go +++ b/graph2/graph.go @@ -34,6 +34,10 @@ func (r *rootResolver) Log() LogResolver { return &types.LogResolver } +func (r *rootResolver) Comment() CommentResolver { + return &types.CommentResolver +} + func (r *rootResolver) Chapter() ChapterResolver { return &types.ChapterResolver } diff --git a/graph2/queries/comment.go b/graph2/queries/comment.go new file mode 100644 index 0000000..309d9b7 --- /dev/null +++ b/graph2/queries/comment.go @@ -0,0 +1,15 @@ +package queries + +import ( + "context" + + "git.aiterp.net/rpdata/api/models/comments" + + "git.aiterp.net/rpdata/api/models" +) + +// Queries + +func (r *resolver) Comment(ctx context.Context, id string) (models.Comment, error) { + return comments.Find(id) +} diff --git a/graph2/schema/root.gql b/graph2/schema/root.gql index 96aa596..892bd08 100644 --- a/graph2/schema/root.gql +++ b/graph2/schema/root.gql @@ -41,6 +41,10 @@ type Query { chapter(id: String!): Chapter! + # Find comment by ID + comment(id: String!): Comment! + + # Find all distinct tags used in stories tags: [Tag!]! diff --git a/graph2/schema/types/Chapter.gql b/graph2/schema/types/Chapter.gql index e4a1be6..36099c9 100644 --- a/graph2/schema/types/Chapter.gql +++ b/graph2/schema/types/Chapter.gql @@ -20,6 +20,35 @@ type Chapter { # The date of edit. editedDate: Date! + + "The comment mode." + commentMode: ChapterCommentMode! + + "Whether the author has locket comments." + commentsLocked: Boolean! + + "A shorthand for checking whether a logged-in user can comment on this chapter." + canComment: Boolean! + + "Get all chapter comments." + comments(limit: Int): [Comment!]! +} + +""" +The possibles modes of comments for a chapter. +""" +enum ChapterCommentMode { + "Comments are disabled and hidden." + Disabled + + "Comments should be shown as typical news article comments." + Article + + "Comments should be shown in a compact form suitable for many small messages." + Chat + + "Comments are going to be shown as omni-tool messages." + Message } # Input for addChapter mutation diff --git a/graph2/schema/types/Comment.gql b/graph2/schema/types/Comment.gql new file mode 100644 index 0000000..dfcf97c --- /dev/null +++ b/graph2/schema/types/Comment.gql @@ -0,0 +1,31 @@ +""" +A Comment represents a comment to a story chapter. +""" +type Comment { + "A unique ID of the change." + id: String! + + "subject" + subject: String! + + "The comment's author." + author: String! + + "The displayed name of the character. This may be the same as character.name, but it does not need to be." + characterName: String! + + "The character associated with the comment." + character: Character + + "The fictional (IC) date of the comment." + fictionalDate: Date + + "The date of creation." + createdDate: Date! + + "The date of the last edit." + editedDate: Date! + + "The markdown source of the comment." + source: String! +} \ No newline at end of file diff --git a/graph2/types/chapter.go b/graph2/types/chapter.go index df1c48c..3c924c9 100644 --- a/graph2/types/chapter.go +++ b/graph2/types/chapter.go @@ -2,9 +2,11 @@ package types import ( "context" + "errors" "time" "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/models/comments" ) type chapterResolver struct{} @@ -17,5 +19,18 @@ func (r *chapterResolver) FictionalDate(ctx context.Context, chapter *models.Cha return &chapter.FictionalDate, nil } +func (r *chapterResolver) Comments(ctx context.Context, chapter *models.Chapter, limit *int) ([]models.Comment, error) { + limitValue := 0 + if limit != nil { + if *limit < 0 { + return nil, errors.New("Limit cannot be negative") + } + + limitValue = *limit + } + + return comments.ListChapterID(chapter.ID, limitValue) +} + // ChapterResolver is a resolver var ChapterResolver chapterResolver diff --git a/graph2/types/comment.go b/graph2/types/comment.go new file mode 100644 index 0000000..77b8641 --- /dev/null +++ b/graph2/types/comment.go @@ -0,0 +1,27 @@ +package types + +import ( + "context" + + "git.aiterp.net/rpdata/api/models/characters" + + "git.aiterp.net/rpdata/api/models" +) + +type commentResolver struct{} + +func (r *commentResolver) Character(ctx context.Context, obj *models.Comment) (*models.Character, error) { + if obj.CharacterID == "" { + return nil, nil + } + + character, err := characters.FindID(obj.CharacterID) + if err != nil { + return nil, err + } + + return &character, nil +} + +// CommentResolver is a resolver +var CommentResolver commentResolver diff --git a/models/chapter-comment-mode.go b/models/chapter-comment-mode.go new file mode 100644 index 0000000..98d9b2f --- /dev/null +++ b/models/chapter-comment-mode.go @@ -0,0 +1,57 @@ +package models + +import ( + "fmt" + "io" +) + +// ChapterCommentMode represents the kind of tags. +type ChapterCommentMode string + +const ( + // ChapterCommentModeDisabled is a chapter comment mode, see GraphQL documentation. + ChapterCommentModeDisabled ChapterCommentMode = "Disabled" + + // ChapterCommentModeArticle is a chapter comment mode, see GraphQL documentation. + ChapterCommentModeArticle ChapterCommentMode = "Article" + + // ChapterCommentModeChat is a chapter comment mode, see GraphQL documentation. + ChapterCommentModeChat ChapterCommentMode = "Chat" + + // ChapterCommentModeMessage is a chapter comment mode, see GraphQL documentation. + ChapterCommentModeMessage ChapterCommentMode = "Message" +) + +// UnmarshalGQL unmarshals +func (e *ChapterCommentMode) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = ChapterCommentMode(str) + switch *e { + case ChapterCommentModeDisabled, ChapterCommentModeArticle, ChapterCommentModeChat, ChapterCommentModeMessage: + return nil + default: + return fmt.Errorf("%s is not a valid ChapterCommentMode", str) + } +} + +// IsEnabled returns true if comments are enabled. +func (e ChapterCommentMode) IsEnabled() bool { + return e != ChapterCommentModeDisabled && len(e) != 0 +} + +// MarshalGQL turns it into a JSON string +func (e ChapterCommentMode) MarshalGQL(w io.Writer) { + // Backwards compatibility: Empty value means disabled. + if len(e) == 0 { + w.Write(disabledChapterCommentModeBytes) + return + } + + fmt.Fprint(w, "\""+string(e)+"\"") +} + +var disabledChapterCommentModeBytes = []byte(`"Disabled"`) diff --git a/models/chapter.go b/models/chapter.go index abf5b7b..e795af7 100644 --- a/models/chapter.go +++ b/models/chapter.go @@ -4,12 +4,19 @@ import "time" // A Chapter is a part of a story. type Chapter struct { - ID string `bson:"_id"` - StoryID string `bson:"storyId"` - Title string `bson:"title"` - Author string `bson:"author"` - Source string `bson:"source"` - CreatedDate time.Time `bson:"createdDate"` - FictionalDate time.Time `bson:"fictionalDate,omitempty"` - EditedDate time.Time `bson:"editedDate"` + ID string `bson:"_id"` + StoryID string `bson:"storyId"` + Title string `bson:"title"` + Author string `bson:"author"` + Source string `bson:"source"` + CreatedDate time.Time `bson:"createdDate"` + FictionalDate time.Time `bson:"fictionalDate,omitempty"` + EditedDate time.Time `bson:"editedDate"` + CommentMode ChapterCommentMode `bson:"commentMode"` + CommentsLocked bool `bson:"commentsLocked"` +} + +// CanComment returns true if the chapter can be commented to. +func (chapter *Chapter) CanComment() bool { + return !chapter.CommentsLocked && chapter.CommentMode.IsEnabled() } diff --git a/models/comment.go b/models/comment.go new file mode 100644 index 0000000..bd003c2 --- /dev/null +++ b/models/comment.go @@ -0,0 +1,17 @@ +package models + +import "time" + +// A Comment is a comment on a chapter. +type Comment struct { + ID string `bson:"id"` + ChapterID string `bson:"chapterId"` + Subject string `bson:"subject"` + Author string `bson:"author"` + CharacterName string `bson:"characterName"` + CharacterID string `bson:"characterId"` + FictionalDate time.Time `bson:"fictionalDate"` + CreatedDate time.Time `bson:"createdDate"` + EditedDate time.Time `bson:"editeddDate"` + Source string `bson:"sources"` +} diff --git a/models/comments/db.go b/models/comments/db.go new file mode 100644 index 0000000..19ff182 --- /dev/null +++ b/models/comments/db.go @@ -0,0 +1,66 @@ +package comments + +import ( + "crypto/rand" + "encoding/binary" + "strconv" + + "git.aiterp.net/rpdata/api/internal/store" + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo" +) + +var collection *mgo.Collection + +func find(query interface{}) (models.Comment, error) { + comment := models.Comment{} + err := collection.Find(query).One(&comment) + + return comment, err +} + +func list(query interface{}, limit int) ([]models.Comment, error) { + allocSize := 32 + if limit >= 0 { + allocSize = limit + } else { + limit = 0 + } + + comments := make([]models.Comment, 0, allocSize) + err := collection.Find(query).Sort("createdDate").Limit(limit).All(&comments) + if err != nil { + return nil, err + } + + return comments, nil +} + +func makeCommentID() string { + result := "SCC" + offset := 0 + data := make([]byte, 48) + + rand.Read(data) + for len(result) < 32 { + result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36) + offset += 8 + + if offset >= 48 { + rand.Read(data) + offset = 0 + } + } + + return result[:32] +} + +func init() { + store.HandleInit(func(db *mgo.Database) { + collection = db.C("story.comments") + + collection.EnsureIndexKey("chapterId") + collection.EnsureIndexKey("author") + collection.EnsureIndexKey("createdDate") + }) +} diff --git a/models/comments/find.go b/models/comments/find.go new file mode 100644 index 0000000..4238fdc --- /dev/null +++ b/models/comments/find.go @@ -0,0 +1,11 @@ +package comments + +import ( + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo/bson" +) + +// Find finds a comment by ID. +func Find(id string) (models.Comment, error) { + return find(bson.M{"_id": id}) +} diff --git a/models/comments/list.go b/models/comments/list.go new file mode 100644 index 0000000..442effe --- /dev/null +++ b/models/comments/list.go @@ -0,0 +1,11 @@ +package comments + +import ( + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo/bson" +) + +// ListChapterID lists all comments by chapter-ID +func ListChapterID(chapterID string, limit int) ([]models.Comment, error) { + return list(bson.M{"chapterId": chapterID}, limit) +}