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:
2026-08-06 22:02:45 +02:00
parent 3f05c97e1f
commit 5a0e2e630b
30 changed files with 4334 additions and 2 deletions
+105
View File
@@ -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] + "..."
}