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