Added a MediaUploadService option to the configuration loader and documented it in the README so operators can pick the attachment backend (defaulting to Litterbox) straight from .env.
All checks were successful
Build & Release / build-latest (push) Successful in 9m53s
Build & Release / version-release (push) Has been skipped

Refactored the Discord bridge to build a media service during initialization, route attachment uploads through it, and dynamically label the status embeds and error diagnostics based on the selected provider while preserving the existing progress messaging flow.

Introduced a new media package that defines the uploader interface and ships a Litterbox implementation responsible for fetching Discord attachments and posting them to Catbox while reporting HTTP status codes back to the bridge.
This commit is contained in:
Salastil
2025-11-18 17:33:42 -05:00
parent 93e875ffb7
commit f954771c0c
6 changed files with 316 additions and 81 deletions

84
media/litterbox.go Normal file
View File

@@ -0,0 +1,84 @@
package media
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
const (
litterboxTTL = "72h"
litterboxEndpoint = "https://litterbox.catbox.moe/resources/internals/api.php"
defaultHTTPTimeout = 60 * time.Second
)
type LitterboxService struct {
client *http.Client
}
func (s *LitterboxService) Name() string { return DefaultService }
func (s *LitterboxService) Upload(ctx context.Context, attachment *discordgo.MessageAttachment) (string, int, error) {
client := s.client
if client == nil {
client = &http.Client{Timeout: defaultHTTPTimeout}
}
if client.Timeout == 0 {
client.Timeout = defaultHTTPTimeout
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, attachment.URL, nil)
if err != nil {
return "", 0, err
}
resp, err := client.Do(req)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", resp.StatusCode, fmt.Errorf("HTTP %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", 0, err
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
_ = writer.WriteField("reqtype", "fileupload")
_ = writer.WriteField("time", litterboxTTL)
part, _ := writer.CreateFormFile("fileToUpload", attachment.Filename)
_, _ = part.Write(data)
_ = writer.Close()
uploadReq, err := http.NewRequestWithContext(ctx, http.MethodPost, litterboxEndpoint, body)
if err != nil {
return "", 0, err
}
uploadReq.Header.Set("Content-Type", writer.FormDataContentType())
uploadResp, err := client.Do(uploadReq)
if err != nil {
return "", 0, err
}
defer uploadResp.Body.Close()
if uploadResp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(uploadResp.Body)
reason := strings.TrimSpace(string(bodyBytes))
if reason != "" {
return "", uploadResp.StatusCode, fmt.Errorf("Litterbox returned HTTP %d: %s", uploadResp.StatusCode, reason)
}
return "", uploadResp.StatusCode, fmt.Errorf("Litterbox returned HTTP %d", uploadResp.StatusCode)
}
out, _ := io.ReadAll(uploadResp.Body)
url := strings.TrimSpace(string(out))
return url, uploadResp.StatusCode, nil
}