okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffeat(cli): restore work and paperclip action shortcutsd98c27e

main
5.4 KiB170 linesraw
1package daemon
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"io"
9	"net/http"
10	"net/url"
11	"slices"
12	"strconv"
13	"strings"
14
15	"albedo/cli/internal/daemon/protocol"
16)
17
18// PreparedPageAction captures the descriptor and target observed before a shortcut runs.
19type PreparedPageAction struct {
20	Page    *PageDocument
21	Request PageActionRequest
22}
23
24type pageShortcut struct {
25	action, id, field, text string
26}
27
28func parsePageShortcut(command, text string) (pageShortcut, error) {
29	verb, rest, _ := strings.Cut(strings.TrimSpace(text), " ")
30	shortcut := pageShortcut{text: strings.TrimSpace(rest)}
31	switch command {
32	case "/work":
33		switch verb {
34		case "add":
35			shortcut.action, shortcut.field = "create", "title"
36		case "edit":
37			shortcut.action, shortcut.field = "edit", "title"
38		case "status":
39			shortcut.action, shortcut.field = "edit", "status"
40		case "remove":
41			shortcut.action = "delete"
42		}
43	case "/paperclips":
44		switch verb {
45		case "acknowledge", "dismiss":
46			shortcut.action = verb
47		case "reply":
48			shortcut.action, shortcut.field = "reply", "reply"
49		case "resolve":
50			shortcut.action, shortcut.field = "resolve", "resolution"
51		case "remove":
52			shortcut.action = "delete"
53		}
54	}
55	if shortcut.action == "" {
56		return shortcut, fmt.Errorf("unknown %s action %q", command, verb)
57	}
58	if shortcut.action != "create" {
59		shortcut.id, shortcut.text, _ = strings.Cut(shortcut.text, " ")
60		shortcut.text = strings.TrimSpace(shortcut.text)
61		id, err := strconv.ParseInt(shortcut.id, 10, 64)
62		if err != nil || id < 1 {
63			return shortcut, errors.New("the action requires a positive item ID")
64		}
65		shortcut.id = strconv.FormatInt(id, 10)
66	}
67	if shortcut.field == "" && shortcut.text != "" {
68		return shortcut, errors.New("this action takes only an item ID")
69	}
70	if shortcut.field != "" && shortcut.field != "resolution" && shortcut.text == "" {
71		return shortcut, fmt.Errorf("%s requires %s", verb, shortcut.field)
72	}
73	return shortcut, nil
74}
75
76// PreparePageShortcut uses the page's own actions and captures off-page targets by ID.
77func PreparePageShortcut(ctx context.Context, conn *Connection, session, command, text string) (*PreparedPageAction, error) {
78	shortcut, err := parsePageShortcut(command, text)
79	if err != nil {
80		return nil, err
81	}
82	doc, err := LoadPage(ctx, conn, session, command)
83	if err != nil {
84		return nil, err
85	}
86	index := slices.IndexFunc(doc.Actions, func(action PageAction) bool { return action.ID == shortcut.action })
87	if index < 0 {
88		return nil, fmt.Errorf("%s does not offer the %s action", command, shortcut.action)
89	}
90	request := PageActionRequest{Action: doc.Actions[index], Session: doc.Session, Form: map[string]json.RawMessage{}}
91	if shortcut.id != "" {
92		row, err := shortcutRow(ctx, conn, doc.Session, command, shortcut.id)
93		if err != nil {
94			return nil, err
95		}
96		request.Row = &row
97		index := slices.IndexFunc(doc.Rows, func(existing PageRow) bool { return existing.ID == row.ID })
98		if index < 0 {
99			doc.Rows = append(doc.Rows, row)
100		} else {
101			doc.Rows[index] = row
102		}
103	}
104	if shortcut.field != "" && shortcut.text != "" {
105		index := slices.IndexFunc(request.Action.Fields, func(field protocol.FormField) bool { return field.Name == shortcut.field })
106		if index < 0 {
107			return nil, fmt.Errorf("the action does not accept %s", shortcut.field)
108		}
109		parsed, err := ParseActionField(request.Action.Fields[index], shortcut.text)
110		if err != nil {
111			return nil, err
112		}
113		request.Form[shortcut.field] = parsed
114	}
115	return &PreparedPageAction{Page: doc, Request: request}, nil
116}
117
118func shortcutRow(ctx context.Context, conn *Connection, session *Session, command, id string) (PageRow, error) {
119	var build func(string, io.Reader) (*http.Request, error)
120	var route string
121	if command == "/work" {
122		build = func(base string, _ io.Reader) (*http.Request, error) {
123			return protocol.NewGetWorkRequest(base, id, &protocol.GetWorkParams{Workspace: session.Workspace})
124		}
125		route = "/extensions/work/items/" + id + "?workspace=" + url.QueryEscape(session.Workspace)
126	} else {
127		build = func(base string, _ io.Reader) (*http.Request, error) {
128			return protocol.NewGetPaperclipsRequest(base, id)
129		}
130		route = "/extensions/paperclips/items/" + id
131	}
132	var etag string
133	data, err := requestBytes(ctx, conn, operation{Name: "read shortcut target", BuildRequest: build, Validator: &etag, Policy: readRecovery}, responseLimits{successStatus: http.StatusOK, bodyBytes: 1048576, errorBytes: 65536})
134	if err != nil {
135		return PageRow{}, err
136	}
137	if etag == "" {
138		return PageRow{}, fieldError("shortcut target validator")
139	}
140	var title, detail, status string
141	if command == "/work" {
142		var item protocol.WorkItem
143		if err := decodeRequired(data, &item); err != nil {
144			return PageRow{}, err
145		}
146		if item.ID != id {
147			return PageRow{}, fieldError("shortcut target identity")
148		}
149		title, detail, status = item.Title, item.Notes, item.Status
150	} else {
151		var item protocol.Paperclip
152		if err := decodeRequired(data, &item); err != nil {
153			return PageRow{}, err
154		}
155		if item.ID != id {
156			return PageRow{}, fieldError("shortcut target identity")
157		}
158		title, detail, status = item.Title, item.Message, item.Status
159		if title == "" {
160			title = item.Message
161		}
162	}
163	row := protocol.PageRow{ID: id, Text: title, Badge: &status, Detail: &detail, Tone: "plain"}
164	row.Resource = &struct {
165		ETag  protocol.ETag        `json:"etag"`
166		URL   protocol.ResourceURL `json:"url"`
167		Value protocol.DynamicJSON `json:"value"`
168	}{etag, route, data}
169	return pageRowValue(row), nil
170}