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 }