Files
m 5d4e064a94
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 15s
DEV-000065 assistant presence attention surface
2026-08-17 16:23:16 +03:00

212 lines
7.4 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/work"
)
var (
errCreatorSessionDenied = errors.New("creator session denied")
errCreatorOwnerDenied = errors.New("creator owner denied")
errCreatorSubjectMissing = errors.New("creator subject not found")
errCreatorQueryUnavailable = errors.New("creator query unavailable")
)
type creatorCoreClient struct {
baseURL *url.URL
client *http.Client
}
type creatorContinuityClient struct {
baseURL *url.URL
client *http.Client
}
func newCreatorCoreClient(rawURL string, client *http.Client) (creatorCoreClient, error) {
baseURL, err := parseCreatorRuntimeURL(rawURL)
return creatorCoreClient{baseURL: baseURL, client: client}, err
}
func newCreatorContinuityClient(rawURL string, client *http.Client) (creatorContinuityClient, error) {
baseURL, err := parseCreatorRuntimeURL(rawURL)
return creatorContinuityClient{baseURL: baseURL, client: client}, err
}
func parseCreatorRuntimeURL(rawURL string) (*url.URL, error) {
baseURL, err := url.Parse(rawURL)
if err != nil || baseURL.Scheme == "" || baseURL.Host == "" {
return nil, fmt.Errorf("invalid Creator runtime URL")
}
return baseURL, nil
}
func (c creatorCoreClient) Session(ctx context.Context, authorization string) (identity.CreatorSession, error) {
var session identity.CreatorSession
status, err := c.get(ctx, "/v1/creator/sessions/current", authorization, &session)
if err != nil {
return identity.CreatorSession{}, err
}
if status == http.StatusUnauthorized {
return identity.CreatorSession{}, errCreatorSessionDenied
}
if status != http.StatusOK || session.ActorID == "" || session.PrincipalID == "" {
return identity.CreatorSession{}, errCreatorQueryUnavailable
}
return session, nil
}
func (c creatorCoreClient) Task(ctx context.Context, id work.TaskID) (work.TaskTarget, error) {
var target work.TaskTarget
status, err := c.get(ctx, "/v1/tasks/"+url.PathEscape(string(id)), "", &target)
if err != nil {
return work.TaskTarget{}, err
}
if status == http.StatusNotFound {
return work.TaskTarget{}, errCreatorSubjectMissing
}
if status != http.StatusOK {
return work.TaskTarget{}, errCreatorQueryUnavailable
}
return target, nil
}
func (c creatorCoreClient) Binding(ctx context.Context, actor identity.ActorID, principal identity.PrincipalID) (identity.PrincipalBinding, error) {
var binding identity.PrincipalBinding
status, err := c.get(ctx, "/v1/principal-bindings/"+url.PathEscape(string(actor))+"/"+url.PathEscape(string(principal)), "", &binding)
if err != nil {
return identity.PrincipalBinding{}, err
}
if status == http.StatusNotFound {
return identity.PrincipalBinding{}, errCreatorOwnerDenied
}
if status != http.StatusOK {
return identity.PrincipalBinding{}, errCreatorQueryUnavailable
}
return binding, nil
}
func (c creatorCoreClient) ActiveCapabilityBinding(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) (capability.Binding, error) {
var binding capability.Binding
target := *c.baseURL
target.Path = "/v1/capability-bindings/active"
query := target.Query()
query.Set("capability_id", string(capabilityID))
query.Set("owner_principal_id", string(owner))
target.RawQuery = query.Encode()
status, err := c.getURL(ctx, target, "", &binding)
if err != nil || status != http.StatusOK {
return capability.Binding{}, errCreatorQueryUnavailable
}
return binding, nil
}
func (c creatorCoreClient) CapabilityBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64) (capability.Binding, error) {
var binding capability.Binding
status, err := c.get(ctx, "/v1/capability-bindings/"+url.PathEscape(string(id))+"/revisions/"+strconv.FormatInt(revision, 10), "", &binding)
if err != nil || status != http.StatusOK {
return capability.Binding{}, errCreatorQueryUnavailable
}
return binding, nil
}
func (c creatorCoreClient) get(ctx context.Context, path, authorization string, destination any) (int, error) {
target := *c.baseURL
target.Path = path
target.RawQuery = ""
return c.getURL(ctx, target, authorization, destination)
}
func (c creatorCoreClient) getURL(ctx context.Context, target url.URL, authorization string, destination any) (int, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
if err != nil {
return 0, errCreatorQueryUnavailable
}
if authorization != "" {
request.Header.Set("Authorization", authorization)
}
response, err := c.client.Do(request)
if err != nil {
return 0, fmt.Errorf("%w: %v", errCreatorQueryUnavailable, err)
}
defer response.Body.Close()
if response.StatusCode == http.StatusOK {
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<10)).Decode(destination); err != nil {
return 0, errCreatorQueryUnavailable
}
}
return response.StatusCode, nil
}
func (c creatorContinuityClient) Item(ctx context.Context, authorization string, id continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
if !strings.HasPrefix(authorization, "Bearer ") {
return continuity.ProspectiveItem{}, errCreatorSessionDenied
}
target := *c.baseURL
target.Path = "/v1/creator/prospective-items/" + url.PathEscape(string(id))
target.RawQuery = ""
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
if err != nil {
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
request.Header.Set("Authorization", authorization)
response, err := c.client.Do(request)
if err != nil {
return continuity.ProspectiveItem{}, fmt.Errorf("%w: %v", errCreatorQueryUnavailable, err)
}
defer response.Body.Close()
switch response.StatusCode {
case http.StatusUnauthorized:
return continuity.ProspectiveItem{}, errCreatorSessionDenied
case http.StatusForbidden:
return continuity.ProspectiveItem{}, errCreatorOwnerDenied
case http.StatusNotFound:
return continuity.ProspectiveItem{}, errCreatorSubjectMissing
case http.StatusOK:
var item continuity.ProspectiveItem
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<10)).Decode(&item); err != nil {
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
return item, nil
default:
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
}
func (c creatorContinuityClient) InternalItem(ctx context.Context, id continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) {
target := *c.baseURL
target.Path = "/v1/prospective-items/" + url.PathEscape(string(id))
target.RawQuery = ""
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
if err != nil {
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
response, err := c.client.Do(request)
if err != nil {
return continuity.ProspectiveItem{}, fmt.Errorf("%w: %v", errCreatorQueryUnavailable, err)
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotFound {
return continuity.ProspectiveItem{}, errCreatorSubjectMissing
}
if response.StatusCode != http.StatusOK {
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
var item continuity.ProspectiveItem
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<10)).Decode(&item); err != nil {
return continuity.ProspectiveItem{}, errCreatorQueryUnavailable
}
return item, nil
}