Files
go-onlyoffice/cmd/oo/docs_comment.go
T
eSlider 5414cc2d37
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 5s
Tests / Test (Go stable) (pull_request) Successful in 2m51s
Tests / Test (Go 1.25) (pull_request) Successful in 2m51s
feat(docs): oo docs comment — add Word comments to a .docx (#88)
2026-10-07 18:31:51 +01:00

157 lines
4.5 KiB
Go

package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
onlyoffice "github.com/eslider/go-onlyoffice"
"github.com/spf13/cobra"
)
func docsCommentCmd() *cobra.Command {
var spec, author, out string
var replace bool
cmd := &cobra.Command{
Use: "comment FILE_ID",
Short: "Add Word comments to an OO .docx, anchored to paragraph text",
Long: `Adds real Word (DOCX) comments to a Documents file and, with --replace,
uploads the annotated copy back over the same file id.
OnlyOffice Workspace exposes no comments REST route (GET/POST
/api/2.0/files/file/{id}/comment answer 404/500), while ONLYOFFICE Docs renders
DOCX comments natively, so the comments are injected into the file itself.
The spec is a JSON array of {"anchor":"<paragraph text>","text":"<comment>"}.
An anchor is matched against the paragraph's concatenated text, so it survives
text split across runs. At most one comment lands on a paragraph.
oo docs comment 4379 --spec comments.json --out review.docx
oo docs comment 4379 --spec comments.json --replace`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if spec == "" {
return fmt.Errorf("--spec is required")
}
raw, err := os.ReadFile(spec)
if err != nil {
return err
}
comments, err := onlyoffice.ParseDocxComments(raw)
if err != nil {
return err
}
if len(comments) == 0 {
return fmt.Errorf("spec %s has no comments", spec)
}
c, err := newOO(cmd)
if err != nil {
return err
}
ctx := cmd.Context()
meta, err := c.GetFile(ctx, args[0])
if err != nil {
return err
}
title := onlyoffice.FileEntryTitle(meta)
if title == "" {
title = "file-" + args[0] + ".docx"
}
if !strings.HasSuffix(strings.ToLower(title), ".docx") {
return fmt.Errorf("%s is not a .docx (comments need Word format)", title)
}
var buf bytes.Buffer
if _, err := c.DownloadFile(ctx, args[0], &buf); err != nil {
return err
}
if author == "" {
author = selfDisplayName(cmd)
}
annotated, applied, err := onlyoffice.InjectDocxComments(buf.Bytes(), author, comments)
if err != nil {
return err
}
if applied == 0 {
return fmt.Errorf("no anchor matched; check the spec against the document text")
}
dest := out
if dest == "" {
base := strings.TrimSuffix(onlyoffice.SafeLocalFileName(title), filepath.Ext(onlyoffice.SafeLocalFileName(title)))
dest = base + " - comments.docx"
}
if err := os.WriteFile(dest, annotated, 0o600); err != nil {
return err
}
obj := map[string]any{
"file_id": args[0],
"title": title,
"comments": len(comments),
"applied": applied,
"author": author,
"local": dest,
"replaced": false,
"onlyoffice_link": c.FileEditorURL(args[0]),
}
if replace {
tmp, err := os.MkdirTemp("", "oo-docs-comment-*")
if err != nil {
return err
}
defer os.RemoveAll(tmp)
up := filepath.Join(tmp, onlyoffice.SafeLocalFileName(title))
if err := os.WriteFile(up, annotated, 0o600); err != nil {
return err
}
// UpdateFile replaces the content in place, keeping the file id
// (so existing links and the CRM entry stay valid).
if _, err := c.UpdateFile(ctx, args[0], up); err != nil {
return err
}
obj["replaced"] = true
obj["folder_id"] = onlyoffice.FileFolderID(meta)
}
printJSON(obj)
return nil
},
}
cmd.Flags().StringVar(&spec, "spec", "", "JSON file: [{\"anchor\":...,\"text\":...}] (required)")
cmd.Flags().StringVar(&author, "author", "", "comment author (default: the authenticated portal user)")
cmd.Flags().StringVar(&out, "out", "", "local output path (default: <title> - comments.docx)")
cmd.Flags().BoolVar(&replace, "replace", false, "upload the annotated copy back over the same file id")
return cmd
}
// selfDisplayName resolves the authenticated user's display name for the
// comment author, falling back to first and last name, then to "OnlyOffice".
func selfDisplayName(cmd *cobra.Command) string {
c, err := newOO(cmd)
if err != nil {
return "OnlyOffice"
}
ctx := cmd.Context()
id, err := c.SelfUserID(ctx)
if err != nil || id == "" {
return "OnlyOffice"
}
u, err := c.GetUser(ctx, id)
if err != nil {
return "OnlyOffice"
}
if s, ok := u["displayName"].(string); ok && strings.TrimSpace(s) != "" {
return s
}
first, _ := u["firstName"].(string)
last, _ := u["lastName"].(string)
if name := strings.TrimSpace(first + " " + last); name != "" {
return name
}
return "OnlyOffice"
}