package forgejo import ( "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "time" ) type Client struct { baseURL string apiToken string httpClient *http.Client } func NewClient(baseURL, apiToken string) *Client { return &Client{ baseURL: baseURL, apiToken: apiToken, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } type CreateIssueRequest struct { Title string `json:"title"` Body string `json:"body"` Labels []int64 `json:"labels,omitempty"` } type Label struct { ID int64 `json:"id"` Name string `json:"name"` } type Issue struct { Number int64 `json:"number"` Title string `json:"title"` State string `json:"state"` } type CreateCommentRequest struct { Body string `json:"body"` } type Comment struct { ID int64 `json:"id"` Body string `json:"body"` } func GenerateWebhookSecret() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil } func (c *Client) GetOrCreateLabel(owner, repo, labelName, color string) (*Label, error) { // Try to find existing label listURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels", c.baseURL, owner, repo) httpReq, err := http.NewRequest("GET", listURL, nil) if err != nil { return nil, err } httpReq.Header.Set("Authorization", "token "+c.apiToken) resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("forgejo API request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { var labels []Label if err := json.NewDecoder(resp.Body).Decode(&labels); err == nil { for _, l := range labels { if l.Name == labelName { return &l, nil } } } } // Create the label createBody, _ := json.Marshal(map[string]string{ "name": labelName, "color": color, }) httpReq, err = http.NewRequest("POST", listURL, bytes.NewReader(createBody)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "token "+c.apiToken) resp2, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("forgejo API request failed: %w", err) } defer resp2.Body.Close() if resp2.StatusCode != http.StatusCreated { respBody, _ := io.ReadAll(resp2.Body) return nil, fmt.Errorf("forgejo API returned %d: %s", resp2.StatusCode, string(respBody)) } var label Label if err := json.NewDecoder(resp2.Body).Decode(&label); err != nil { return nil, err } return &label, nil } func (c *Client) CreateIssue(owner, repo string, req CreateIssueRequest) (*Issue, error) { url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues", c.baseURL, owner, repo) body, err := json.Marshal(req) if err != nil { return nil, err } httpReq, err := http.NewRequest("POST", url, bytes.NewReader(body)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "token "+c.apiToken) resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("forgejo API request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("forgejo API returned %d: %s", resp.StatusCode, string(respBody)) } var issue Issue if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil { return nil, err } return &issue, nil } func (c *Client) CreateComment(owner, repo string, issueNumber int64, req CreateCommentRequest) (*Comment, error) { url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d/comments", c.baseURL, owner, repo, issueNumber) body, err := json.Marshal(req) if err != nil { return nil, err } httpReq, err := http.NewRequest("POST", url, bytes.NewReader(body)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "token "+c.apiToken) resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("forgejo API request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("forgejo API returned %d: %s", resp.StatusCode, string(respBody)) } var comment Comment if err := json.NewDecoder(resp.Body).Decode(&comment); err != nil { return nil, err } return &comment, nil }