diff options
| author | s <[email protected]> | 2025-11-03 21:17:12 -0500 |
|---|---|---|
| committer | s <[email protected]> | 2025-11-03 21:17:12 -0500 |
| commit | f7fcfa623e670dc533bb378912829c73a3593e63 (patch) | |
| tree | 910119ff7293b407affa9ff34706d627d77a3a04 /internal/client | |
| download | dborg-f7fcfa623e670dc533bb378912829c73a3593e63.tar.gz dborg-f7fcfa623e670dc533bb378912829c73a3593e63.zip | |
hi
Diffstat (limited to 'internal/client')
| -rw-r--r-- | internal/client/admin.go | 124 | ||||
| -rw-r--r-- | internal/client/client.go | 119 | ||||
| -rw-r--r-- | internal/client/client_test.go | 46 | ||||
| -rw-r--r-- | internal/client/npd.go | 91 | ||||
| -rw-r--r-- | internal/client/osint.go | 32 | ||||
| -rw-r--r-- | internal/client/skiptrace.go | 179 | ||||
| -rw-r--r-- | internal/client/sl.go | 53 | ||||
| -rw-r--r-- | internal/client/usrsx.go | 72 | ||||
| -rw-r--r-- | internal/client/x.go | 23 |
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 +} |
