diff --git a/atproto/api.go b/atproto/api.go deleted file mode 100644 index 8a76f29..0000000 --- a/atproto/api.go +++ /dev/null @@ -1,53 +0,0 @@ -package atproto - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/bluesky-social/indigo/atproto/atclient" - "github.com/bluesky-social/indigo/atproto/syntax" -) - -// APIResponse contains the response returned by an authenticated XRPC call. -type APIResponse struct { - StatusCode int - Body []byte -} - -// CallAPI performs an authenticated XRPC request. -func CallAPI(ctx context.Context, client *atclient.APIClient, method string, endpoint syntax.NSID, fields map[string]any) (*APIResponse, error) { - request := atclient.NewAPIRequest(method, endpoint, nil) - request.Headers.Set("Accept", "application/json") - if method == http.MethodGet { - request.QueryParams = apiQuery(fields) - } else { - body, err := json.Marshal(fields) - if err != nil { - return nil, fmt.Errorf("encode request body: %w", err) - } - request.Body = bytes.NewReader(body) - request.Headers.Set("Content-Type", "application/json") - } - response, err := client.Do(ctx, request) - if err != nil { - return nil, fmt.Errorf("call %s: %w", endpoint, err) - } - defer response.Body.Close() - body, err := io.ReadAll(response.Body) - if err != nil { - return nil, fmt.Errorf("read API response: %w", err) - } - return &APIResponse{StatusCode: response.StatusCode, Body: body}, nil -} - -func apiQuery(fields map[string]any) map[string][]string { - query := make(map[string][]string, len(fields)) - for key, value := range fields { - query[key] = []string{fmt.Sprint(value)} - } - return query -} diff --git a/internal/app/api.go b/internal/app/api.go deleted file mode 100644 index 347549b..0000000 --- a/internal/app/api.go +++ /dev/null @@ -1,33 +0,0 @@ -package app - -import ( - "context" - "fmt" - - "github.com/alyraffauf/tg/atproto" - "github.com/bluesky-social/indigo/atproto/syntax" -) - -// APIRequestInput describes an authenticated XRPC request. -type APIRequestInput struct { - Endpoint string - Method string - Fields map[string]any -} - -// APIResponse contains the response returned by an authenticated XRPC call. -type APIResponse = atproto.APIResponse - -// CallAPI performs an authenticated XRPC request for frontend-specific API -// commands. -func (s *Service) CallAPI(ctx context.Context, in APIRequestInput) (*APIResponse, error) { - endpoint, err := syntax.ParseNSID(in.Endpoint) - if err != nil { - return nil, fmt.Errorf("parse NSID: %w", err) - } - client, err := s.sessions.APIClient(ctx) - if err != nil { - return nil, err - } - return atproto.CallAPI(ctx, client, in.Method, endpoint, in.Fields) -} diff --git a/internal/app/service_test.go b/internal/app/service_test.go index c8332b9..8f16870 100644 --- a/internal/app/service_test.go +++ b/internal/app/service_test.go @@ -179,45 +179,6 @@ func TestDownloadPullPatch(t *testing.T) { } } -func TestCallAPIValidatesEndpointBeforeAuthenticating(t *testing.T) { - service := testService(&testPDS{}, &testGit{}, &testKnot{}) - _, err := service.CallAPI(context.Background(), APIRequestInput{Endpoint: "not an nsid", Method: http.MethodGet}) - if err == nil || !strings.HasPrefix(err.Error(), "parse NSID") { - t.Fatalf("CallAPI() error = %v, want NSID validation error", err) - } -} - -func TestCallAPIPostsJSON(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - if request.Method != http.MethodPost || request.URL.Path != "/xrpc/com.example.test" { - t.Fatalf("request = %s %s", request.Method, request.URL.Path) - } - body, err := io.ReadAll(request.Body) - if err != nil { - t.Fatalf("read request body: %v", err) - } - if got, want := string(body), `{"message":"hello"}`; got != want { - t.Fatalf("request body = %q, want %q", got, want) - } - writer.WriteHeader(http.StatusCreated) - _, _ = writer.Write([]byte(`{"ok":true}`)) - })) - defer server.Close() - - service := testService(&testPDS{}, &testGit{}, &testKnot{}) - service.sessions = testSessions{pds: &testPDS{}, api: &atclient.APIClient{Host: server.URL, Client: server.Client()}} - - response, err := service.CallAPI(context.Background(), APIRequestInput{ - Endpoint: "com.example.test", Method: http.MethodPost, Fields: map[string]any{"message": "hello"}, - }) - if err != nil { - t.Fatalf("CallAPI() error = %v", err) - } - if response.StatusCode != http.StatusCreated || string(response.Body) != `{"ok":true}` { - t.Fatalf("CallAPI() response = %+v", response) - } -} - func gzipContents(t *testing.T, contents []byte) []byte { t.Helper() var compressed bytes.Buffer diff --git a/internal/cli/api.go b/internal/cli/api.go deleted file mode 100644 index 16dd42f..0000000 --- a/internal/cli/api.go +++ /dev/null @@ -1,84 +0,0 @@ -package cli - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/alyraffauf/tg/internal/app" - "github.com/spf13/cobra" -) - -func newAPICommand(service *app.Service) *cobra.Command { - var methodFlag string - var fieldsFlag []string - - command := &cobra.Command{ - Use: "api ", - Short: "Call an authenticated XRPC endpoint", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - fields, err := parseAPIFields(fieldsFlag) - if err != nil { - return err - } - method := strings.ToUpper(methodFlag) - if method == "GET" && len(fieldsFlag) > 0 && !cmd.Flags().Changed("method") { - method = "POST" - } - if method != http.MethodGet && method != http.MethodPost { - return fmt.Errorf("method must be GET or POST, got %q", methodFlag) - } - - response, err := service.CallAPI(cmd.Context(), app.APIRequestInput{ - Endpoint: args[0], - Method: method, - Fields: fields, - }) - if err != nil { - return err - } - return writeAPIResponse(cmd, response) - }, - } - command.Flags().StringVarP(&methodFlag, "method", "X", "GET", "HTTP method (GET or POST)") - command.Flags().StringArrayVarP(&fieldsFlag, "field", "f", nil, "Add a key=value field") - return command -} - -func parseAPIFields(rawFields []string) (map[string]any, error) { - fields := make(map[string]any, len(rawFields)) - for _, rawField := range rawFields { - key, value, found := strings.Cut(rawField, "=") - if !found || key == "" { - return nil, fmt.Errorf("field must be key=value, got %q", rawField) - } - - var decoded any - if err := json.Unmarshal([]byte(value), &decoded); err == nil { - fields[key] = decoded - } else { - fields[key] = value - } - } - return fields, nil -} - -func writeAPIResponse(cmd *cobra.Command, response *app.APIResponse) error { - if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { - return fmt.Errorf("API returned HTTP %d: %s", response.StatusCode, strings.TrimSpace(string(response.Body))) - } - - if json.Valid(response.Body) { - var formatted bytes.Buffer - if err := json.Indent(&formatted, response.Body, "", " "); err == nil { - formatted.WriteByte('\n') - _, err = cmd.OutOrStdout().Write(formatted.Bytes()) - return err - } - } - _, err := cmd.OutOrStdout().Write(response.Body) - return err -} diff --git a/internal/cli/root.go b/internal/cli/root.go index 0dfd015..8cdc845 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -58,7 +58,7 @@ func newRoot(service *app.Service, defaultKnot, defaultSSHPort, defaultProtocol stringsCmd := newStringCommand(service) stringsCmd.AddCommand(newStringCreateCommand(service), newStringListCommand(service), newStringViewCommand(service), newStringDeleteCommand(service)) - rootCmd.AddCommand(stringsCmd, newBrowseCommand(service), newCompletionCommand(service), newManCommand(service), newAPICommand(service)) + rootCmd.AddCommand(stringsCmd, newBrowseCommand(service), newCompletionCommand(service), newManCommand(service)) return rootCmd }