Add self-hosted RCS backend, extension, and ops tooling
Ship the Go/SQLite API and Web UI, Chrome/Brave capture addon, Docker Compose, Pangolin reverse-proxy support, and a user-crontab watchdog so the binary stays running without systemd.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package imgur
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const uploadURL = "https://api.imgur.com/3/image"
|
||||
|
||||
// Client uploads images to Imgur anonymously with a Client-ID.
|
||||
type Client struct {
|
||||
clientID string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// New creates an Imgur client. clientID may be empty (uploads will fail clearly).
|
||||
func New(clientID string) *Client {
|
||||
return &Client{
|
||||
clientID: clientID,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled reports whether a Client-ID is configured.
|
||||
func (c *Client) Enabled() bool {
|
||||
return c != nil && c.clientID != ""
|
||||
}
|
||||
|
||||
type uploadResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
Link string `json:"link"`
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// UploadFile uploads a local image file and returns the public Imgur URL.
|
||||
func (c *Client) UploadFile(path string) (string, error) {
|
||||
if !c.Enabled() {
|
||||
return "", fmt.Errorf("imgur is not configured (set IMGUR_CLIENT_ID)")
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open screenshot: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
part, err := writer.CreateFormFile("image", "screenshot.png")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = writer.WriteField("type", "file")
|
||||
if err := writer.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, uploadURL, &body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Client-ID "+c.clientID)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("imgur request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var parsed uploadResponse
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return "", fmt.Errorf("decode imgur response: %w (body=%s)", err, truncate(string(raw), 200))
|
||||
}
|
||||
if !parsed.Success || parsed.Data.Link == "" {
|
||||
return "", fmt.Errorf("imgur upload failed (status=%d): %s", resp.StatusCode, truncate(string(raw), 300))
|
||||
}
|
||||
return parsed.Data.Link, nil
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user