1package visual
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "log/slog"
10 "net/http"
11 "time"
12
13 lexutil "github.com/bluesky-social/indigo/lex/util"
14 "github.com/bluesky-social/indigo/util"
15
16 "github.com/carlmjohnson/versioninfo"
17)
18
19type AbyssClient struct {
20 Client http.Client
21 Host string
22 Password string
23 RatelimitBypass string
24}
25
26func NewAbyssClient(host, password, ratelimitBypass string) AbyssClient {
27 return AbyssClient{
28 Client: *util.RobustHTTPClient(),
29 Host: host,
30 Password: password,
31 RatelimitBypass: password,
32 }
33}
34
35func (ac *AbyssClient) ScanBlob(ctx context.Context, blob lexutil.LexBlob, blobBytes []byte, params map[string]string) (*AbyssScanResp, error) {
36
37 slog.Debug("sending blob to abyss", "cid", blob.Ref.String(), "mimetype", blob.MimeType, "size", len(blobBytes))
38
39 body := bytes.NewBuffer(blobBytes)
40 req, err := http.NewRequest("POST", ac.Host+"/xrpc/com.atproto.unspecced.scanBlob", body)
41 if err != nil {
42 return nil, err
43 }
44
45 q := req.URL.Query()
46 for k, v := range params {
47 q.Add(k, v)
48 }
49 req.URL.RawQuery = q.Encode()
50
51 req.SetBasicAuth("admin", ac.Password)
52 req.Header.Add("Content-Type", blob.MimeType)
53 req.Header.Add("Content-Length", fmt.Sprintf("%d", blob.Size))
54 req.Header.Set("Accept", "application/json")
55 req.Header.Set("User-Agent", "indigo-automod/"+versioninfo.Short())
56 if ac.RatelimitBypass != "" {
57 req.Header.Set("x-ratelimit-bypass", ac.RatelimitBypass)
58 }
59
60 start := time.Now()
61 defer func() {
62 duration := time.Since(start)
63 abyssAPIDuration.Observe(duration.Seconds())
64 }()
65
66 req = req.WithContext(ctx)
67 res, err := ac.Client.Do(req)
68 if err != nil {
69 return nil, fmt.Errorf("abyss request failed: %v", err)
70 }
71 defer res.Body.Close()
72
73 abyssAPICount.WithLabelValues(fmt.Sprint(res.StatusCode)).Inc()
74 if res.StatusCode != 200 {
75 return nil, fmt.Errorf("abyss request failed statusCode=%d", res.StatusCode)
76 }
77
78 respBytes, err := io.ReadAll(res.Body)
79 if err != nil {
80 return nil, fmt.Errorf("failed to read abyss resp body: %v", err)
81 }
82
83 var respObj AbyssScanResp
84 if err := json.Unmarshal(respBytes, &respObj); err != nil {
85 return nil, fmt.Errorf("failed to parse abyss resp JSON: %v", err)
86 }
87 slog.Info("abyss-scan-response", "cid", blob.Ref.String(), "obj", respObj)
88 return &respObj, nil
89}