261 lines
7.4 KiB
Go
261 lines
7.4 KiB
Go
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, err := io.ReadAll(resp.Body)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("gotify API error (HTTP %d): failed to read body: %w", resp.StatusCode, err)
|
||
|
|
}
|
||
|
|
return fmt.Errorf("gotify API error (HTTP %d): %s", resp.StatusCode, string(body))
|
||
|
|
}
|