summaryrefslogtreecommitdiffstats
path: root/internal/client
diff options
context:
space:
mode:
authors <[email protected]>2025-11-03 21:17:12 -0500
committers <[email protected]>2025-11-03 21:17:12 -0500
commitf7fcfa623e670dc533bb378912829c73a3593e63 (patch)
tree910119ff7293b407affa9ff34706d627d77a3a04 /internal/client
downloaddborg-f7fcfa623e670dc533bb378912829c73a3593e63.tar.gz
dborg-f7fcfa623e670dc533bb378912829c73a3593e63.zip
hi
Diffstat (limited to 'internal/client')
-rw-r--r--internal/client/admin.go124
-rw-r--r--internal/client/client.go119
-rw-r--r--internal/client/client_test.go46
-rw-r--r--internal/client/npd.go91
-rw-r--r--internal/client/osint.go32
-rw-r--r--internal/client/skiptrace.go179
-rw-r--r--internal/client/sl.go53
-rw-r--r--internal/client/usrsx.go72
-rw-r--r--internal/client/x.go23
9 files changed, 739 insertions, 0 deletions
diff --git a/internal/client/admin.go b/internal/client/admin.go
new file mode 100644
index 0000000..a5a9519
--- /dev/null
+++ b/internal/client/admin.go
@@ -0,0 +1,124 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) ListAccounts() (*models.AdminResponse, error) {
+ data, err := c.Get("/admin/accounts", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) CreateAccount(req *models.AccountCreateRequest) (*models.AdminResponse, error) {
+ data, err := c.Post("/admin/accounts", req)
+ if err != nil {
+ return nil, err
+ }
+
+ var account models.Account
+ if err := json.Unmarshal(data, &account); err == nil && account.APIKey != "" {
+ return &models.AdminResponse{
+ Success: true,
+ Account: &account,
+ }, nil
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) DeleteAccount(apiKey string) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s", url.PathEscape(apiKey))
+ data, err := c.Delete(path)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) UpdateCredits(apiKey string, credits int) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/credits", url.PathEscape(apiKey))
+ req := &models.AddCreditsRequest{
+ Credits: credits,
+ }
+
+ data, err := c.Post(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SetCredits(apiKey string, credits int) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/credits", url.PathEscape(apiKey))
+ req := &models.SetCreditsRequest{
+ Credits: credits,
+ }
+
+ data, err := c.Put(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var account models.Account
+ if err := json.Unmarshal(data, &account); err == nil && account.APIKey != "" {
+ return &models.AdminResponse{
+ Success: true,
+ Account: &account,
+ }, nil
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) ToggleAccount(apiKey string, enable bool) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/disable", url.PathEscape(apiKey))
+ req := &models.DisableAccountRequest{
+ Disabled: !enable,
+ }
+
+ data, err := c.Patch(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/client.go b/internal/client/client.go
new file mode 100644
index 0000000..098479f
--- /dev/null
+++ b/internal/client/client.go
@@ -0,0 +1,119 @@
+package client
+
+import (
+ "bytes"
+ "dborg/internal/config"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+type Client struct {
+ config *config.Config
+ httpClient *http.Client
+}
+
+func New(cfg *config.Config) (*Client, error) {
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+
+ return &Client{
+ config: cfg,
+ httpClient: &http.Client{
+ Timeout: cfg.Timeout,
+ },
+ }, nil
+}
+
+func (c *Client) doRequest(method, path string, params url.Values, body interface{}) ([]byte, error) {
+ fullURL := c.config.BaseURL + path
+ if params != nil && len(params) > 0 {
+ fullURL += "?" + params.Encode()
+ }
+
+ var reqBody io.Reader
+ if body != nil {
+ jsonData, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal request body: %w", err)
+ }
+ reqBody = bytes.NewBuffer(jsonData)
+ }
+
+ req, err := http.NewRequest(method, fullURL, reqBody)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ var resp *http.Response
+ var lastErr error
+
+ for attempt := 0; attempt <= c.config.MaxRetries; attempt++ {
+ if attempt > 0 {
+ time.Sleep(time.Duration(attempt) * time.Second)
+ }
+
+ resp, err = c.httpClient.Do(req)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
+ return io.ReadAll(resp.Body)
+ }
+
+ bodyBytes, _ := io.ReadAll(resp.Body)
+
+ switch resp.StatusCode {
+ case http.StatusForbidden:
+ lastErr = fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes))
+ case http.StatusUnauthorized:
+ lastErr = fmt.Errorf("unauthorized (401): %s - Check your API key", string(bodyBytes))
+ case http.StatusTooManyRequests:
+ lastErr = fmt.Errorf("rate limit exceeded (429): %s", string(bodyBytes))
+ case http.StatusBadRequest:
+ lastErr = fmt.Errorf("bad request (400): %s", string(bodyBytes))
+ default:
+ lastErr = fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ if resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < 500 {
+ break
+ }
+ }
+
+ return nil, lastErr
+}
+
+func (c *Client) Get(path string, params url.Values) ([]byte, error) {
+ return c.doRequest(http.MethodGet, path, params, nil)
+}
+
+func (c *Client) Post(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPost, path, nil, body)
+}
+
+func (c *Client) Delete(path string) ([]byte, error) {
+ return c.doRequest(http.MethodDelete, path, nil, nil)
+}
+
+func (c *Client) Patch(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPatch, path, nil, body)
+}
+
+func (c *Client) Put(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPut, path, nil, body)
+}
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
new file mode 100644
index 0000000..9bf453d
--- /dev/null
+++ b/internal/client/client_test.go
@@ -0,0 +1,46 @@
+package client
+
+import (
+ "dborg/internal/config"
+ "testing"
+ "time"
+)
+
+func TestNewClient(t *testing.T) {
+ tests := []struct {
+ name string
+ config *config.Config
+ wantErr bool
+ }{
+ {
+ name: "valid config",
+ config: &config.Config{
+ APIKey: "test-key",
+ BaseURL: "https://db.org.ai",
+ Timeout: 30 * time.Second,
+ MaxRetries: 3,
+ UserAgent: "test-agent",
+ },
+ wantErr: false,
+ },
+ {
+ name: "missing API key",
+ config: &config.Config{
+ BaseURL: "https://db.org.ai",
+ Timeout: 30 * time.Second,
+ MaxRetries: 3,
+ UserAgent: "test-agent",
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := New(tt.config)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/internal/client/npd.go b/internal/client/npd.go
new file mode 100644
index 0000000..c63327b
--- /dev/null
+++ b/internal/client/npd.go
@@ -0,0 +1,91 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchNPD(params *models.NPDParams) (*models.NPDResponse, error) {
+ queryParams := url.Values{}
+
+ if params.ID != "" {
+ queryParams.Add("id", params.ID)
+ }
+ if params.FirstName != "" {
+ queryParams.Add("firstname", params.FirstName)
+ }
+ if params.LastName != "" {
+ queryParams.Add("lastname", params.LastName)
+ }
+ if params.MiddleName != "" {
+ queryParams.Add("middlename", params.MiddleName)
+ }
+ if params.DOB != "" {
+ queryParams.Add("dob", params.DOB)
+ }
+ if params.SSN != "" {
+ queryParams.Add("ssn", params.SSN)
+ }
+ if params.Phone1 != "" {
+ queryParams.Add("phone1", params.Phone1)
+ }
+ if params.Address != "" {
+ queryParams.Add("address", params.Address)
+ }
+ if params.City != "" {
+ queryParams.Add("city", params.City)
+ }
+ if params.State != "" {
+ queryParams.Add("st", params.State)
+ }
+ if params.Zip != "" {
+ queryParams.Add("zip", params.Zip)
+ }
+ if params.CountyName != "" {
+ queryParams.Add("county_name", params.CountyName)
+ }
+ if params.NameSuffix != "" {
+ queryParams.Add("name_suff", params.NameSuffix)
+ }
+ if params.AKA1FullName != "" {
+ queryParams.Add("aka1fullname", params.AKA1FullName)
+ }
+ if params.AKA2FullName != "" {
+ queryParams.Add("aka2fullname", params.AKA2FullName)
+ }
+ if params.AKA3FullName != "" {
+ queryParams.Add("aka3fullname", params.AKA3FullName)
+ }
+ if params.Alt1DOB != "" {
+ queryParams.Add("alt1dob", params.Alt1DOB)
+ }
+ if params.Alt2DOB != "" {
+ queryParams.Add("alt2dob", params.Alt2DOB)
+ }
+ if params.Alt3DOB != "" {
+ queryParams.Add("alt3dob", params.Alt3DOB)
+ }
+ if params.StartDate != "" {
+ queryParams.Add("startdat", params.StartDate)
+ }
+ if params.MaxHits > 0 && params.MaxHits != 10 {
+ queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits))
+ }
+ if params.SortBy != "" {
+ queryParams.Add("sort_by", params.SortBy)
+ }
+
+ data, err := c.Get("/npd/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.NPDResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse NPD response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/osint.go b/internal/client/osint.go
new file mode 100644
index 0000000..70dbb72
--- /dev/null
+++ b/internal/client/osint.go
@@ -0,0 +1,32 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) LookupBSSID(params *models.BSSIDParams) (*models.BSSIDLookupResponse, error) {
+ path := fmt.Sprintf("/osint/bssid/%s", url.PathEscape(params.BSSID))
+
+ queryParams := url.Values{}
+ if params.All {
+ queryParams.Add("all", "true")
+ }
+ if params.Map {
+ queryParams.Add("map", "true")
+ }
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.BSSIDLookupResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse BSSID lookup response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/skiptrace.go b/internal/client/skiptrace.go
new file mode 100644
index 0000000..b1d5008
--- /dev/null
+++ b/internal/client/skiptrace.go
@@ -0,0 +1,179 @@
+package client
+
+import (
+ "bufio"
+ "bytes"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+func parseSSEResponse(data []byte) ([]byte, error) {
+ scanner := bufio.NewScanner(bytes.NewReader(data))
+
+ const maxScanTokenSize = 10 * 1024 * 1024
+ buf := make([]byte, maxScanTokenSize)
+ scanner.Buffer(buf, maxScanTokenSize)
+
+ var resultData []byte
+ var foundResult bool
+
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if line == "event: result" {
+ foundResult = true
+ continue
+ }
+
+ if foundResult && strings.HasPrefix(line, "data: ") {
+ resultData = []byte(strings.TrimPrefix(line, "data: "))
+ break
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("error reading SSE response: %w", err)
+ }
+
+ if resultData == nil {
+ return nil, fmt.Errorf("no result event found in SSE response")
+ }
+
+ trimmed := strings.TrimSpace(string(resultData))
+ if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
+ return nil, fmt.Errorf("API returned: %s", trimmed)
+ }
+
+ return resultData, nil
+}
+
+func (c *Client) getSSE(path string, params url.Values) ([]byte, error) {
+ fullURL := c.config.BaseURL + path
+ if params != nil && len(params) > 0 {
+ fullURL += "?" + params.Encode()
+ }
+
+ req, err := http.NewRequest("GET", fullURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("request failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ switch resp.StatusCode {
+ case http.StatusForbidden:
+ return nil, fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes))
+ case http.StatusUnauthorized:
+ return nil, fmt.Errorf("unauthorized (401): %s - Check your API key", string(bodyBytes))
+ case http.StatusTooManyRequests:
+ return nil, fmt.Errorf("rate limit exceeded (429): %s", string(bodyBytes))
+ case http.StatusBadRequest:
+ return nil, fmt.Errorf("bad request (400): %s", string(bodyBytes))
+ default:
+ return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+ }
+
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response: %w", err)
+ }
+
+ contentType := resp.Header.Get("Content-Type")
+
+ if strings.HasPrefix(string(data), "event:") || strings.Contains(contentType, "text/event-stream") {
+ return parseSSEResponse(data)
+ }
+
+ return data, nil
+}
+
+func (c *Client) SearchPeople(params *models.SkiptraceParams) (*models.SkiptraceResponse, error) {
+ queryParams := url.Values{}
+ queryParams.Set("first_name", params.FirstName)
+ queryParams.Set("last_name", params.LastName)
+
+ if params.City != "" {
+ queryParams.Set("city", params.City)
+ }
+ if params.State != "" {
+ queryParams.Set("state", params.State)
+ }
+ if params.Age != "" {
+ queryParams.Set("age", params.Age)
+ }
+
+ data, err := c.getSSE("/prem/skiptrace/people/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) GetPersonReport(sxKey string, selection int) (*models.SkiptraceReportResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/people/report/%s/%d", sxKey, selection)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceReportResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SearchPhone(phone string) (*models.SkiptracePhoneResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/phone/%s", phone)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptracePhoneResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SearchEmail(email string) (*models.SkiptraceEmailResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/email/%s", email)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceEmailResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/sl.go b/internal/client/sl.go
new file mode 100644
index 0000000..fb3b270
--- /dev/null
+++ b/internal/client/sl.go
@@ -0,0 +1,53 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchStealerLogs(params *models.SLParams) (*models.SLResponse, error) {
+ queryParams := url.Values{}
+ queryParams.Add("query", params.Query)
+
+ if params.MaxHits > 0 && params.MaxHits != 10 {
+ queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits))
+ }
+ if params.SortBy != "" {
+ queryParams.Add("sort_by", params.SortBy)
+ }
+ if params.IngestStartDate != "" {
+ queryParams.Add("ingest_start_date", params.IngestStartDate)
+ }
+ if params.IngestEndDate != "" {
+ queryParams.Add("ingest_end_date", params.IngestEndDate)
+ }
+ if params.PostedStartDate != "" {
+ queryParams.Add("posted_start_date", params.PostedStartDate)
+ }
+ if params.PostedEndDate != "" {
+ queryParams.Add("posted_end_date", params.PostedEndDate)
+ }
+ if params.Format != "" && params.Format != "json" {
+ queryParams.Add("format", params.Format)
+ }
+
+ data, err := c.Get("/sl/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ if params.Format != "" && params.Format != "json" {
+ return &models.SLResponse{
+ Message: string(data),
+ }, nil
+ }
+
+ var response models.SLResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse stealer logs response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/usrsx.go b/internal/client/usrsx.go
new file mode 100644
index 0000000..456acbf
--- /dev/null
+++ b/internal/client/usrsx.go
@@ -0,0 +1,72 @@
+package client
+
+import (
+ "bufio"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+func (c *Client) CheckUsernameStream(params *models.USRSXParams, callback func(result json.RawMessage) error) error {
+ queryParams := url.Values{}
+
+ if len(params.Sites) > 0 {
+ queryParams.Add("sites", strings.Join(params.Sites, ","))
+ }
+ if params.Fuzzy {
+ queryParams.Add("fuzzy", "true")
+ }
+ if params.MaxTasks > 0 && params.MaxTasks != 50 {
+ queryParams.Add("max_tasks", fmt.Sprintf("%d", params.MaxTasks))
+ }
+
+ path := fmt.Sprintf("/osint/username/%s", url.PathEscape(params.Username))
+ fullURL := c.config.BaseURL + path
+ if len(queryParams) > 0 {
+ fullURL += "?" + queryParams.Encode()
+ }
+
+ req, err := http.NewRequest(http.MethodGet, fullURL, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+ req.Header.Set("Accept", "application/x-ndjson, application/json")
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to execute request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
+ }
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ line := scanner.Bytes()
+ if len(line) == 0 {
+ continue
+ }
+
+ if err := callback(json.RawMessage(line)); err != nil {
+ return err
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ if !strings.Contains(err.Error(), "context deadline exceeded") && !strings.Contains(err.Error(), "timeout") {
+ return fmt.Errorf("stream reading error: %w", err)
+ }
+ }
+
+ return nil
+}
diff --git a/internal/client/x.go b/internal/client/x.go
new file mode 100644
index 0000000..8bdb21c
--- /dev/null
+++ b/internal/client/x.go
@@ -0,0 +1,23 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchTwitterHistory(username string) (*models.XResponse, error) {
+ path := fmt.Sprintf("/x/search/%s", url.PathEscape(username))
+ data, err := c.Get(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.XResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse Twitter/X response: %w", err)
+ }
+
+ return &response, nil
+}