package notification import ( "bytes" "encoding/json" "fmt" "io" "net/http" "time" ) // GotifyClient is a CRUD client for the Gotify API. type GotifyClient struct { baseURL string appToken string // token for sending messages (application token) clientToken string // token for reading/managing (client token) httpClient *http.Client } // NewGotifyClient creates a Gotify API client. // appToken is used for sending messages. // clientToken is used for listing/deleting messages and managing applications. func NewGotifyClient(baseURL, appToken, clientToken string) *GotifyClient { return &GotifyClient{ baseURL: baseURL, appToken: appToken, clientToken: clientToken, httpClient: &http.Client{ Timeout: 10 * time.Second, }, } } // --- Message Types --- // GotifyMessage represents a Gotify message. type GotifyMessage struct { ID int `json:"id,omitempty"` AppID int `json:"appid,omitempty"` Title string `json:"title"` Message string `json:"message"` Priority int `json:"priority,omitempty"` Date string `json:"date,omitempty"` Extras map[string]interface{} `json:"extras,omitempty"` } // GotifyMessageList is a paginated list of messages. type GotifyMessageList struct { Messages []GotifyMessage `json:"messages"` Paging GotifyPaging `json:"paging"` } // GotifyPaging represents pagination info. type GotifyPaging struct { Size int `json:"size"` Since int `json:"since"` Limit int `json:"limit"` Next string `json:"next,omitempty"` } // --- Application Types --- // GotifyApplication represents a Gotify application. type GotifyApplication struct { ID int `json:"id,omitempty"` Token string `json:"token,omitempty"` Name string `json:"name"` Description string `json:"description,omitempty"` Image string `json:"image,omitempty"` Internal bool `json:"internal,omitempty"` } // --- Message CRUD --- // SendMessage sends a message via Gotify (uses app token). func (c *GotifyClient) SendMessage(msg GotifyMessage) (*GotifyMessage, error) { body, err := json.Marshal(msg) if err != nil { return nil, fmt.Errorf("marshal message: %w", err) } req, err := http.NewRequest(http.MethodPost, c.baseURL+"/message", bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Gotify-Key", c.appToken) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("send message: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { return nil, c.readError(resp) } var result GotifyMessage if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode response: %w", err) } return &result, nil } // ListMessages lists messages (uses client token). func (c *GotifyClient) ListMessages(limit int) (*GotifyMessageList, error) { url := fmt.Sprintf("%s/message?limit=%d", c.baseURL, limit) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("X-Gotify-Key", c.clientToken) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("list messages: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, c.readError(resp) } var result GotifyMessageList if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode response: %w", err) } return &result, nil } // DeleteMessage deletes a message by ID (uses client token). func (c *GotifyClient) DeleteMessage(id int) error { url := fmt.Sprintf("%s/message/%d", c.baseURL, id) req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { return fmt.Errorf("create request: %w", err) } req.Header.Set("X-Gotify-Key", c.clientToken) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("delete message: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { return c.readError(resp) } return nil } // DeleteAllMessages deletes all messages (uses client token). func (c *GotifyClient) DeleteAllMessages() error { req, err := http.NewRequest(http.MethodDelete, c.baseURL+"/message", nil) if err != nil { return fmt.Errorf("create request: %w", err) } req.Header.Set("X-Gotify-Key", c.clientToken) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("delete all messages: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { return c.readError(resp) } return nil } // --- Application CRUD --- // ListApplications lists all applications (uses client token). func (c *GotifyClient) ListApplications() ([]GotifyApplication, error) { req, err := http.NewRequest(http.MethodGet, c.baseURL+"/application", nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("X-Gotify-Key", c.clientToken) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("list applications: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, c.readError(resp) } var result []GotifyApplication if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode response: %w", err) } return result, nil } // CreateApplication creates a new application (uses client token). func (c *GotifyClient) CreateApplication(app GotifyApplication) (*GotifyApplication, error) { body, err := json.Marshal(app) if err != nil { return nil, fmt.Errorf("marshal application: %w", err) } req, err := http.NewRequest(http.MethodPost, c.baseURL+"/application", bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Gotify-Key", c.clientToken) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("create application: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { return nil, c.readError(resp) } var result GotifyApplication if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode response: %w", err) } return &result, nil } // DeleteApplication deletes an application by ID (uses client token). func (c *GotifyClient) DeleteApplication(id int) error { url := fmt.Sprintf("%s/application/%d", c.baseURL, id) req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { return fmt.Errorf("create request: %w", err) } req.Header.Set("X-Gotify-Key", c.clientToken) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("delete application: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { return c.readError(resp) } return nil } // --- Helpers --- func (c *GotifyClient) readError(resp *http.Response) error { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("gotify API error (HTTP %d): %s", resp.StatusCode, string(body)) }