gogcli/internal/cmd/docs_comments.go
2026-03-09 00:40:28 +00:00

256 lines
7.7 KiB
Go

package cmd
import (
"context"
"fmt"
"strings"
"github.com/steipete/gogcli/internal/ui"
)
// DocsCommentsCmd is the parent command for comment operations on a Google Doc.
type DocsCommentsCmd struct {
List DocsCommentsListCmd `cmd:"" name:"list" aliases:"ls" help:"List comments on a Google Doc"`
Get DocsCommentsGetCmd `cmd:"" name:"get" aliases:"info,show" help:"Get a comment by ID"`
Add DocsCommentsAddCmd `cmd:"" name:"add" aliases:"create,new" help:"Add a comment to a Google Doc"`
Reply DocsCommentsReplyCmd `cmd:"" name:"reply" aliases:"respond" help:"Reply to a comment"`
Resolve DocsCommentsResolveCmd `cmd:"" name:"resolve" help:"Resolve a comment (mark as done)"`
Delete DocsCommentsDeleteCmd `cmd:"" name:"delete" aliases:"rm,del,remove" help:"Delete a comment"`
}
// DocsCommentsListCmd lists comments on a Google Doc.
type DocsCommentsListCmd struct {
DocID string `arg:"" name:"docId" help:"Google Doc ID or URL"`
IncludeResolved bool `name:"include-resolved" aliases:"resolved" help:"Include resolved comments (default: open only)"`
Max int64 `name:"max" aliases:"limit" help:"Max results per page" default:"100"`
Page string `name:"page" aliases:"cursor" help:"Page token for pagination"`
All bool `name:"all" aliases:"all-pages" help:"Fetch all pages"`
FailEmpty bool `name:"fail-empty" aliases:"non-empty,require-results" help:"Exit with code 3 if no results"`
}
func (c *DocsCommentsListCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := normalizeGoogleID(strings.TrimSpace(c.DocID))
if docID == "" {
return usage("empty docId")
}
if c.Max <= 0 {
return usage("max must be > 0")
}
_, svc, err := requireDriveService(ctx, flags)
if err != nil {
return err
}
comments, nextPageToken, err := listDriveComments(ctx, svc, docID, driveCommentListOptions{
resourceKey: "docId",
resourceID: docID,
includeResolved: c.IncludeResolved,
scanForOpen: true,
page: c.Page,
all: c.All,
failEmpty: c.FailEmpty,
max: c.Max,
emptyMessage: "No comments",
mode: driveCommentListModeExpanded,
})
if err != nil {
return err
}
return writeDriveCommentList(ctx, u, driveCommentListOptions{
resourceKey: "docId",
resourceID: docID,
failEmpty: c.FailEmpty,
emptyMessage: "No comments",
mode: driveCommentListModeExpanded,
}, comments, nextPageToken)
}
// DocsCommentsGetCmd retrieves a single comment by ID.
type DocsCommentsGetCmd struct {
DocID string `arg:"" name:"docId" help:"Google Doc ID or URL"`
CommentID string `arg:"" name:"commentId" help:"Comment ID"`
}
func (c *DocsCommentsGetCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := normalizeGoogleID(strings.TrimSpace(c.DocID))
commentID := strings.TrimSpace(c.CommentID)
if docID == "" {
return usage("empty docId")
}
if commentID == "" {
return usage("empty commentId")
}
_, svc, err := requireDriveService(ctx, flags)
if err != nil {
return err
}
comment, err := getDriveComment(ctx, svc, docID, commentID)
if err != nil {
return err
}
return writeDriveCommentDetail(ctx, u, comment, true, true)
}
// DocsCommentsAddCmd creates a comment on a Google Doc.
type DocsCommentsAddCmd struct {
DocID string `arg:"" name:"docId" help:"Google Doc ID or URL"`
Content string `arg:"" name:"content" help:"Comment text"`
Quoted string `name:"quoted" help:"Quoted text to attach to the comment (shown in UIs when available)"`
Anchor string `name:"anchor" help:"Anchor JSON string (advanced; editor UIs may still treat as unanchored)"`
}
func (c *DocsCommentsAddCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := normalizeGoogleID(strings.TrimSpace(c.DocID))
content := strings.TrimSpace(c.Content)
quoted := strings.TrimSpace(c.Quoted)
anchor := strings.TrimSpace(c.Anchor)
if docID == "" {
return usage("empty docId")
}
if content == "" {
return usage("empty content")
}
if err := dryRunExit(ctx, flags, "docs.comments.add", map[string]any{
"doc_id": docID,
"content": content,
"quoted": quoted,
"anchor": anchor,
}); err != nil {
return err
}
_, svc, err := requireDriveService(ctx, flags)
if err != nil {
return err
}
created, err := createDriveComment(ctx, svc, docID, content, quoted, anchor)
if err != nil {
return err
}
return writeDriveCommentMutation(ctx, u, created, true)
}
// DocsCommentsReplyCmd replies to a comment on a Google Doc.
type DocsCommentsReplyCmd struct {
DocID string `arg:"" name:"docId" help:"Google Doc ID or URL"`
CommentID string `arg:"" name:"commentId" help:"Comment ID"`
Content string `arg:"" name:"content" help:"Reply text"`
}
func (c *DocsCommentsReplyCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := normalizeGoogleID(strings.TrimSpace(c.DocID))
commentID := strings.TrimSpace(c.CommentID)
content := strings.TrimSpace(c.Content)
if docID == "" {
return usage("empty docId")
}
if commentID == "" {
return usage("empty commentId")
}
if content == "" {
return usage("empty content")
}
if err := dryRunExit(ctx, flags, "docs.comments.reply", map[string]any{
"doc_id": docID,
"comment_id": commentID,
"content": content,
}); err != nil {
return err
}
_, svc, err := requireDriveService(ctx, flags)
if err != nil {
return err
}
created, err := createDriveReply(ctx, svc, docID, commentID, content)
if err != nil {
return err
}
return writeDriveReplyMutation(ctx, u, created, false, "", "", "")
}
// DocsCommentsResolveCmd resolves a comment by posting an empty reply with action "resolve".
// The Drive API resolves a comment when a reply is created with action="resolve".
type DocsCommentsResolveCmd struct {
DocID string `arg:"" name:"docId" help:"Google Doc ID or URL"`
CommentID string `arg:"" name:"commentId" help:"Comment ID"`
Message string `name:"message" short:"m" help:"Optional message to include when resolving"`
}
func (c *DocsCommentsResolveCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := normalizeGoogleID(strings.TrimSpace(c.DocID))
commentID := strings.TrimSpace(c.CommentID)
if docID == "" {
return usage("empty docId")
}
if commentID == "" {
return usage("empty commentId")
}
if err := dryRunExit(ctx, flags, "docs.comments.resolve", map[string]any{
"doc_id": docID,
"comment_id": commentID,
}); err != nil {
return err
}
_, svc, err := requireDriveService(ctx, flags)
if err != nil {
return err
}
created, err := resolveDriveComment(ctx, svc, docID, commentID, c.Message)
if err != nil {
return err
}
return writeDriveReplyMutation(ctx, u, created, true, "docId", docID, commentID)
}
// DocsCommentsDeleteCmd deletes a comment on a Google Doc.
type DocsCommentsDeleteCmd struct {
DocID string `arg:"" name:"docId" help:"Google Doc ID or URL"`
CommentID string `arg:"" name:"commentId" help:"Comment ID"`
}
func (c *DocsCommentsDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := normalizeGoogleID(strings.TrimSpace(c.DocID))
commentID := strings.TrimSpace(c.CommentID)
if docID == "" {
return usage("empty docId")
}
if commentID == "" {
return usage("empty commentId")
}
if confirmErr := confirmDestructive(ctx, flags, fmt.Sprintf("delete comment %s from doc %s", commentID, docID)); confirmErr != nil {
return confirmErr
}
_, svc, err := requireDriveService(ctx, flags)
if err != nil {
return err
}
if err := deleteDriveComment(ctx, svc, docID, commentID); err != nil {
return err
}
return writeResult(ctx, u,
kv("deleted", true),
kv("docId", docID),
kv("commentId", commentID),
)
}