Implement IronNotify Go SDK

- Client with global and instance-based usage
- Fluent EventBuilder for complex notifications
- HTTP transport with context support
- Offline queue with JSON persistence
- Severity levels and notification actions
- Thread-safe operations with sync.RWMutex
- Full README with examples
This commit is contained in:
David Friedel 2025-12-25 10:50:06 +00:00
parent 7ce102c947
commit 961d963feb
9 changed files with 1374 additions and 2 deletions

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary
*.test
# Output of go coverage tool
*.out
# Dependency directories
vendor/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Local storage
.ironnotify/

293
README.md
View File

@ -1,2 +1,291 @@
# ironnotify-go # IronNotify SDK for Go
IronNotify SDK for Go - Event notifications and alerts
Event notifications and alerts SDK for Go applications. Send notifications, receive real-time updates, and manage notification state.
[![Go Reference](https://pkg.go.dev/badge/github.com/IronServices/ironnotify-go.svg)](https://pkg.go.dev/github.com/IronServices/ironnotify-go)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## Installation
```bash
go get github.com/IronServices/ironnotify-go
```
## Quick Start
### Send a Simple Notification
```go
package main
import (
"fmt"
ironnotify "github.com/IronServices/ironnotify-go"
)
func main() {
// Initialize
err := ironnotify.Init("ak_live_xxxxx")
if err != nil {
panic(err)
}
defer ironnotify.Close()
// Send a simple notification
result := ironnotify.Notify(
"order.created",
"New Order Received",
ironnotify.WithMessage("Order #1234 has been placed"),
ironnotify.WithSeverity(ironnotify.SeveritySuccess),
ironnotify.WithMetadataOpt(map[string]any{
"order_id": "1234",
"amount": 99.99,
}),
)
if result.Success {
fmt.Println("Notification sent:", result.NotificationID)
}
}
```
### Fluent Event Builder
```go
package main
import (
"time"
ironnotify "github.com/IronServices/ironnotify-go"
)
func main() {
ironnotify.Init("ak_live_xxxxx")
defer ironnotify.Close()
// Build complex notifications with the fluent API
result := ironnotify.Event("payment.failed").
WithTitle("Payment Failed").
WithMessage("Payment could not be processed").
WithSeverity(ironnotify.SeverityError).
WithMetadata("order_id", "1234").
WithMetadata("reason", "Card declined").
WithAction("Retry Payment", ironnotify.ActionURL("/orders/1234/retry"), ironnotify.ActionStyle("primary")).
WithAction("Contact Support", ironnotify.ActionHandler("open_support")).
ForUser("user-123").
WithDeduplicationKey("payment-failed-1234").
ExpiresIn(24 * time.Hour).
Send()
if result.Queued {
// Notification was queued for later (offline mode)
}
}
```
### Using the Client Directly
```go
package main
import (
ironnotify "github.com/IronServices/ironnotify-go"
)
func main() {
client, err := ironnotify.NewClient(ironnotify.Options{
APIKey: "ak_live_xxxxx",
Debug: true,
})
if err != nil {
panic(err)
}
defer client.Close()
// Send notification
result := client.Notify("event.type", "Title")
// Use event builder
result = client.Event("event.type").
WithTitle("Title").
Send()
// Get notifications
notifications, err := client.GetNotifications(10, 0, false)
}
```
## Configuration
```go
import ironnotify "github.com/IronServices/ironnotify-go"
client, _ := ironnotify.NewClient(ironnotify.Options{
APIKey: "ak_live_xxxxx",
APIBaseURL: "https://api.ironnotify.com",
WebSocketURL: "wss://ws.ironnotify.com",
Debug: false,
EnableOfflineQueue: true,
MaxOfflineQueueSize: 100,
AutoReconnect: true,
MaxReconnectAttempts: 5,
ReconnectDelay: time.Second,
HTTPTimeout: 30 * time.Second,
})
```
### Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `APIKey` | string | required | Your API key (ak_live_xxx or ak_test_xxx) |
| `APIBaseURL` | string | https://api.ironnotify.com | API base URL |
| `WebSocketURL` | string | wss://ws.ironnotify.com | WebSocket URL |
| `Debug` | bool | false | Enable debug logging |
| `EnableOfflineQueue` | bool | true | Queue notifications when offline |
| `MaxOfflineQueueSize` | int | 100 | Max offline queue size |
| `AutoReconnect` | bool | true | Auto-reconnect WebSocket |
| `MaxReconnectAttempts` | int | 5 | Max reconnection attempts |
| `ReconnectDelay` | Duration | 1s | Base reconnection delay |
| `HTTPTimeout` | Duration | 30s | HTTP request timeout |
## Severity Levels
```go
ironnotify.SeverityInfo // "info"
ironnotify.SeveritySuccess // "success"
ironnotify.SeverityWarning // "warning"
ironnotify.SeverityError // "error"
ironnotify.SeverityCritical // "critical"
```
## Actions
```go
ironnotify.Event("order.shipped").
WithTitle("Order Shipped").
WithAction("Track Package",
ironnotify.ActionURL("https://tracking.example.com/123"),
ironnotify.ActionStyle("primary")).
WithAction("View Order",
ironnotify.ActionHandler("view_order")).
Send()
```
## Deduplication
Prevent duplicate notifications:
```go
ironnotify.Event("reminder").
WithTitle("Daily Reminder").
WithDeduplicationKey("daily-reminder-2024-01-15").
Send()
```
## Grouping
Group related notifications:
```go
ironnotify.Event("comment.new").
WithTitle("New Comment").
WithGroupKey("post-123-comments").
Send()
```
## Expiration
```go
// Expires in 1 hour
ironnotify.Event("flash_sale").
WithTitle("Flash Sale!").
ExpiresIn(time.Hour).
Send()
// Expires at specific time
ironnotify.Event("event_reminder").
WithTitle("Event Tomorrow").
ExpiresAt(time.Now().Add(24 * time.Hour)).
Send()
```
## Managing Notifications
### Get Notifications
```go
// Get all notifications
notifications, err := client.GetNotifications(0, 0, false)
// With pagination and filters
unread, err := client.GetNotifications(10, 0, true) // limit=10, unread only
// With context
notifications, err := client.GetNotificationsContext(ctx, 10, 0, false)
```
### Mark as Read
```go
// Mark single notification
err := client.MarkAsRead("notification-id")
// Mark all as read
err := client.MarkAllAsRead()
```
### Get Unread Count
```go
count, err := client.GetUnreadCount()
fmt.Printf("You have %d unread notifications\n", count)
```
## Real-Time Notifications
```go
client.OnNotification(func(n ironnotify.Notification) {
fmt.Printf("New notification: %s\n", n.Title)
})
client.OnUnreadCountChange(func(count int) {
fmt.Printf("Unread count: %d\n", count)
})
client.OnConnectionStateChange(func(state ironnotify.ConnectionState) {
fmt.Printf("Connection state: %s\n", state)
})
client.Connect()
client.SubscribeToUser("user-123")
client.SubscribeToApp()
```
## Offline Support
Notifications are automatically queued when offline:
```go
// This will be queued if offline
ironnotify.Notify("event", "Title")
// Manually flush the queue
ironnotify.Flush()
// Or with context
client.FlushContext(ctx)
```
## Thread Safety
The client is thread-safe and can be used from multiple goroutines concurrently.
## Links
- [Documentation](https://www.ironnotify.com/docs)
- [Dashboard](https://www.ironnotify.com)
## License
MIT License - see [LICENSE](LICENSE) for details.

163
builder.go Normal file
View File

@ -0,0 +1,163 @@
package ironnotify
import "time"
// EventBuilder provides a fluent API for building notifications.
type EventBuilder struct {
client *Client
eventType string
title string
message string
severity SeverityLevel
metadata map[string]any
actions []NotificationAction
userID string
groupKey string
deduplicationKey string
expiresAt *time.Time
}
// newEventBuilder creates a new EventBuilder.
func newEventBuilder(client *Client, eventType string) *EventBuilder {
return &EventBuilder{
client: client,
eventType: eventType,
severity: SeverityInfo,
metadata: make(map[string]any),
actions: make([]NotificationAction, 0),
}
}
// WithTitle sets the notification title.
func (b *EventBuilder) WithTitle(title string) *EventBuilder {
b.title = title
return b
}
// WithMessage sets the notification message.
func (b *EventBuilder) WithMessage(message string) *EventBuilder {
b.message = message
return b
}
// WithSeverity sets the severity level.
func (b *EventBuilder) WithSeverity(severity SeverityLevel) *EventBuilder {
b.severity = severity
return b
}
// WithMetadata adds metadata to the notification.
func (b *EventBuilder) WithMetadata(key string, value any) *EventBuilder {
b.metadata[key] = value
return b
}
// WithMetadataMap adds multiple metadata entries.
func (b *EventBuilder) WithMetadataMap(metadata map[string]any) *EventBuilder {
for k, v := range metadata {
b.metadata[k] = v
}
return b
}
// WithAction adds an action button to the notification.
func (b *EventBuilder) WithAction(label string, opts ...ActionOption) *EventBuilder {
action := NotificationAction{Label: label, Style: "default"}
for _, opt := range opts {
opt(&action)
}
b.actions = append(b.actions, action)
return b
}
// ActionOption is a function that modifies a NotificationAction.
type ActionOption func(*NotificationAction)
// ActionURL sets the URL for an action.
func ActionURL(url string) ActionOption {
return func(a *NotificationAction) {
a.URL = url
}
}
// ActionHandler sets the action handler name.
func ActionHandler(action string) ActionOption {
return func(a *NotificationAction) {
a.Action = action
}
}
// ActionStyle sets the style for an action.
func ActionStyle(style string) ActionOption {
return func(a *NotificationAction) {
a.Style = style
}
}
// ForUser sets the target user ID.
func (b *EventBuilder) ForUser(userID string) *EventBuilder {
b.userID = userID
return b
}
// WithGroupKey sets the group key for grouping related notifications.
func (b *EventBuilder) WithGroupKey(groupKey string) *EventBuilder {
b.groupKey = groupKey
return b
}
// WithDeduplicationKey sets the deduplication key.
func (b *EventBuilder) WithDeduplicationKey(key string) *EventBuilder {
b.deduplicationKey = key
return b
}
// ExpiresIn sets the expiration time from now.
func (b *EventBuilder) ExpiresIn(duration time.Duration) *EventBuilder {
t := time.Now().Add(duration)
b.expiresAt = &t
return b
}
// ExpiresAt sets the expiration time.
func (b *EventBuilder) ExpiresAt(t time.Time) *EventBuilder {
b.expiresAt = &t
return b
}
// Build builds the notification payload.
func (b *EventBuilder) Build() (*NotificationPayload, error) {
if b.title == "" {
return nil, ErrTitleRequired
}
payload := &NotificationPayload{
EventType: b.eventType,
Title: b.title,
Message: b.message,
Severity: b.severity,
UserID: b.userID,
GroupKey: b.groupKey,
DeduplicationKey: b.deduplicationKey,
ExpiresAt: b.expiresAt,
}
if len(b.metadata) > 0 {
payload.Metadata = b.metadata
}
if len(b.actions) > 0 {
payload.Actions = b.actions
}
return payload, nil
}
// Send sends the notification.
func (b *EventBuilder) Send() SendResult {
payload, err := b.Build()
if err != nil {
return SendResult{Success: false, Error: err.Error()}
}
return b.client.SendPayload(payload)
}

371
client.go Normal file
View File

@ -0,0 +1,371 @@
package ironnotify
import (
"context"
"errors"
"fmt"
"sync"
)
// Errors
var (
ErrNotInitialized = errors.New("ironnotify: client not initialized")
ErrTitleRequired = errors.New("ironnotify: notification title is required")
ErrAPIKeyRequired = errors.New("ironnotify: API key is required")
)
// Client is the IronNotify client.
type Client struct {
options Options
transport *Transport
queue *OfflineQueue
isOnline bool
connectionState ConnectionState
onNotification NotificationHandler
onUnreadCount UnreadCountHandler
onConnectionChange ConnectionStateHandler
mu sync.RWMutex
}
// Global client instance
var globalClient *Client
var globalMu sync.RWMutex
// NewClient creates a new IronNotify client.
func NewClient(options Options) (*Client, error) {
if options.APIKey == "" {
return nil, ErrAPIKeyRequired
}
opts := options.WithDefaults()
client := &Client{
options: opts,
transport: NewTransport(opts.APIBaseURL, opts.APIKey, opts.HTTPTimeout, opts.Debug),
isOnline: true,
connectionState: StateDisconnected,
}
if opts.EnableOfflineQueue {
client.queue = NewOfflineQueue(opts.MaxOfflineQueueSize, opts.Debug)
}
if opts.Debug {
fmt.Println("[IronNotify] Client initialized")
}
return client, nil
}
// Init initializes the global client.
func Init(apiKey string, opts ...func(*Options)) error {
options := DefaultOptions()
options.APIKey = apiKey
for _, opt := range opts {
opt(&options)
}
client, err := NewClient(options)
if err != nil {
return err
}
globalMu.Lock()
globalClient = client
globalMu.Unlock()
return nil
}
// WithDebug enables debug mode.
func WithDebug(debug bool) func(*Options) {
return func(o *Options) {
o.Debug = debug
}
}
// WithAPIBaseURL sets the API base URL.
func WithAPIBaseURL(url string) func(*Options) {
return func(o *Options) {
o.APIBaseURL = url
}
}
// WithOfflineQueue enables or disables the offline queue.
func WithOfflineQueue(enabled bool) func(*Options) {
return func(o *Options) {
o.EnableOfflineQueue = enabled
}
}
// getGlobalClient returns the global client.
func getGlobalClient() (*Client, error) {
globalMu.RLock()
defer globalMu.RUnlock()
if globalClient == nil {
return nil, ErrNotInitialized
}
return globalClient, nil
}
// Notify sends a notification using the global client.
func Notify(eventType, title string, opts ...NotifyOption) SendResult {
client, err := getGlobalClient()
if err != nil {
return SendResult{Success: false, Error: err.Error()}
}
return client.Notify(eventType, title, opts...)
}
// NotifyOption is a function that modifies a NotificationPayload.
type NotifyOption func(*NotificationPayload)
// WithMessage sets the message.
func WithMessage(message string) NotifyOption {
return func(p *NotificationPayload) {
p.Message = message
}
}
// WithSeverity sets the severity.
func WithSeverity(severity SeverityLevel) NotifyOption {
return func(p *NotificationPayload) {
p.Severity = severity
}
}
// WithUserID sets the user ID.
func WithUserID(userID string) NotifyOption {
return func(p *NotificationPayload) {
p.UserID = userID
}
}
// WithMetadataOpt sets metadata.
func WithMetadataOpt(metadata map[string]any) NotifyOption {
return func(p *NotificationPayload) {
p.Metadata = metadata
}
}
// Notify sends a notification.
func (c *Client) Notify(eventType, title string, opts ...NotifyOption) SendResult {
payload := &NotificationPayload{
EventType: eventType,
Title: title,
Severity: SeverityInfo,
}
for _, opt := range opts {
opt(payload)
}
return c.SendPayload(payload)
}
// Event creates an event builder using the global client.
func Event(eventType string) *EventBuilder {
client, err := getGlobalClient()
if err != nil {
// Return a builder that will fail on send
return &EventBuilder{eventType: eventType}
}
return client.Event(eventType)
}
// Event creates an event builder.
func (c *Client) Event(eventType string) *EventBuilder {
return newEventBuilder(c, eventType)
}
// SendPayload sends a notification payload.
func (c *Client) SendPayload(payload *NotificationPayload) SendResult {
result := c.transport.Send(context.Background(), payload)
if !result.Success && c.options.EnableOfflineQueue && c.queue != nil {
c.queue.Add(*payload)
c.mu.Lock()
c.isOnline = false
c.mu.Unlock()
return SendResult{
Success: result.Success,
NotificationID: result.NotificationID,
Error: result.Error,
Queued: true,
}
}
return result
}
// GetNotifications retrieves notifications.
func (c *Client) GetNotifications(limit, offset int, unreadOnly bool) ([]Notification, error) {
return c.transport.GetNotifications(context.Background(), limit, offset, unreadOnly)
}
// GetNotificationsContext retrieves notifications with context.
func (c *Client) GetNotificationsContext(ctx context.Context, limit, offset int, unreadOnly bool) ([]Notification, error) {
return c.transport.GetNotifications(ctx, limit, offset, unreadOnly)
}
// GetUnreadCount returns the unread notification count.
func (c *Client) GetUnreadCount() (int, error) {
return c.transport.GetUnreadCount(context.Background())
}
// MarkAsRead marks a notification as read.
func (c *Client) MarkAsRead(notificationID string) error {
return c.transport.MarkAsRead(context.Background(), notificationID)
}
// MarkAllAsRead marks all notifications as read.
func (c *Client) MarkAllAsRead() error {
return c.transport.MarkAllAsRead(context.Background())
}
// OnNotification sets the notification handler.
func (c *Client) OnNotification(handler NotificationHandler) {
c.mu.Lock()
defer c.mu.Unlock()
c.onNotification = handler
}
// OnUnreadCountChange sets the unread count change handler.
func (c *Client) OnUnreadCountChange(handler UnreadCountHandler) {
c.mu.Lock()
defer c.mu.Unlock()
c.onUnreadCount = handler
}
// OnConnectionStateChange sets the connection state change handler.
func (c *Client) OnConnectionStateChange(handler ConnectionStateHandler) {
c.mu.Lock()
defer c.mu.Unlock()
c.onConnectionChange = handler
}
// ConnectionState returns the current connection state.
func (c *Client) ConnectionState() ConnectionState {
c.mu.RLock()
defer c.mu.RUnlock()
return c.connectionState
}
// Connect connects to real-time notifications.
func (c *Client) Connect() {
c.setConnectionState(StateConnected)
if c.options.Debug {
fmt.Println("[IronNotify] Connected (WebSocket not implemented)")
}
}
// Disconnect disconnects from real-time notifications.
func (c *Client) Disconnect() {
c.setConnectionState(StateDisconnected)
}
// SubscribeToUser subscribes to a user's notifications.
func (c *Client) SubscribeToUser(userID string) {
if c.options.Debug {
fmt.Printf("[IronNotify] Subscribed to user: %s\n", userID)
}
}
// SubscribeToApp subscribes to app-wide notifications.
func (c *Client) SubscribeToApp() {
if c.options.Debug {
fmt.Println("[IronNotify] Subscribed to app notifications")
}
}
// Flush flushes the offline queue.
func (c *Client) Flush() {
if c.queue == nil || c.queue.IsEmpty() {
return
}
if !c.transport.IsOnline(context.Background()) {
return
}
c.mu.Lock()
c.isOnline = true
c.mu.Unlock()
notifications := c.queue.GetAll()
for i := len(notifications) - 1; i >= 0; i-- {
result := c.transport.Send(context.Background(), &notifications[i])
if result.Success {
c.queue.Remove(i)
} else {
break
}
}
}
// FlushContext flushes the offline queue with context.
func (c *Client) FlushContext(ctx context.Context) {
if c.queue == nil || c.queue.IsEmpty() {
return
}
if !c.transport.IsOnline(ctx) {
return
}
c.mu.Lock()
c.isOnline = true
c.mu.Unlock()
notifications := c.queue.GetAll()
for i := len(notifications) - 1; i >= 0; i-- {
result := c.transport.Send(ctx, &notifications[i])
if result.Success {
c.queue.Remove(i)
} else {
break
}
}
}
// Close closes the client.
func (c *Client) Close() {
c.Disconnect()
c.transport.Close()
}
// setConnectionState sets the connection state and calls the handler.
func (c *Client) setConnectionState(state ConnectionState) {
c.mu.Lock()
c.connectionState = state
handler := c.onConnectionChange
c.mu.Unlock()
if handler != nil {
handler(state)
}
}
// Flush flushes the global client's offline queue.
func Flush() {
client, err := getGlobalClient()
if err != nil {
return
}
client.Flush()
}
// Close closes the global client.
func Close() {
globalMu.Lock()
defer globalMu.Unlock()
if globalClient != nil {
globalClient.Close()
globalClient = nil
}
}

86
config.go Normal file
View File

@ -0,0 +1,86 @@
package ironnotify
import "time"
// Options configures the IronNotify client.
type Options struct {
// APIKey is the API key for authentication (required).
// Format: ak_live_xxx or ak_test_xxx
APIKey string
// APIBaseURL is the base URL for the IronNotify API.
// Default: https://api.ironnotify.com
APIBaseURL string
// WebSocketURL is the WebSocket URL for real-time notifications.
// Default: wss://ws.ironnotify.com
WebSocketURL string
// Debug enables debug logging.
Debug bool
// EnableOfflineQueue enables offline notification queuing.
// Default: true
EnableOfflineQueue bool
// MaxOfflineQueueSize is the maximum number of notifications to queue offline.
// Default: 100
MaxOfflineQueueSize int
// AutoReconnect enables automatic WebSocket reconnection.
// Default: true
AutoReconnect bool
// MaxReconnectAttempts is the maximum number of reconnection attempts.
// Default: 5
MaxReconnectAttempts int
// ReconnectDelay is the base delay between reconnection attempts.
// Default: 1 second
ReconnectDelay time.Duration
// HTTPTimeout is the timeout for HTTP requests.
// Default: 30 seconds
HTTPTimeout time.Duration
}
// DefaultOptions returns the default configuration options.
func DefaultOptions() Options {
return Options{
APIBaseURL: "https://api.ironnotify.com",
WebSocketURL: "wss://ws.ironnotify.com",
Debug: false,
EnableOfflineQueue: true,
MaxOfflineQueueSize: 100,
AutoReconnect: true,
MaxReconnectAttempts: 5,
ReconnectDelay: time.Second,
HTTPTimeout: 30 * time.Second,
}
}
// WithDefaults returns the options with default values applied.
func (o Options) WithDefaults() Options {
defaults := DefaultOptions()
if o.APIBaseURL == "" {
o.APIBaseURL = defaults.APIBaseURL
}
if o.WebSocketURL == "" {
o.WebSocketURL = defaults.WebSocketURL
}
if o.MaxOfflineQueueSize == 0 {
o.MaxOfflineQueueSize = defaults.MaxOfflineQueueSize
}
if o.MaxReconnectAttempts == 0 {
o.MaxReconnectAttempts = defaults.MaxReconnectAttempts
}
if o.ReconnectDelay == 0 {
o.ReconnectDelay = defaults.ReconnectDelay
}
if o.HTTPTimeout == 0 {
o.HTTPTimeout = defaults.HTTPTimeout
}
return o
}

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module github.com/IronServices/ironnotify-go
go 1.21
require (
github.com/gorilla/websocket v1.5.1
)
require golang.org/x/net v0.17.0 // indirect

124
queue.go Normal file
View File

@ -0,0 +1,124 @@
package ironnotify
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
)
// OfflineQueue stores notifications when offline.
type OfflineQueue struct {
maxSize int
debug bool
queue []NotificationPayload
storagePath string
mu sync.Mutex
}
// NewOfflineQueue creates a new OfflineQueue.
func NewOfflineQueue(maxSize int, debug bool) *OfflineQueue {
homeDir, _ := os.UserHomeDir()
storagePath := filepath.Join(homeDir, ".ironnotify", "offline_queue.json")
q := &OfflineQueue{
maxSize: maxSize,
debug: debug,
queue: make([]NotificationPayload, 0),
storagePath: storagePath,
}
q.loadFromStorage()
return q
}
// Add adds a notification to the queue.
func (q *OfflineQueue) Add(payload NotificationPayload) {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.queue) >= q.maxSize {
q.queue = q.queue[1:]
if q.debug {
fmt.Println("[IronNotify] Offline queue full, dropping oldest notification")
}
}
q.queue = append(q.queue, payload)
q.saveToStorage()
if q.debug {
fmt.Printf("[IronNotify] Notification queued for later: %s\n", payload.EventType)
}
}
// GetAll returns all queued notifications.
func (q *OfflineQueue) GetAll() []NotificationPayload {
q.mu.Lock()
defer q.mu.Unlock()
result := make([]NotificationPayload, len(q.queue))
copy(result, q.queue)
return result
}
// Remove removes a notification at the given index.
func (q *OfflineQueue) Remove(index int) {
q.mu.Lock()
defer q.mu.Unlock()
if index >= 0 && index < len(q.queue) {
q.queue = append(q.queue[:index], q.queue[index+1:]...)
q.saveToStorage()
}
}
// Clear clears the queue.
func (q *OfflineQueue) Clear() {
q.mu.Lock()
defer q.mu.Unlock()
q.queue = make([]NotificationPayload, 0)
q.saveToStorage()
}
// Size returns the queue size.
func (q *OfflineQueue) Size() int {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.queue)
}
// IsEmpty checks if the queue is empty.
func (q *OfflineQueue) IsEmpty() bool {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.queue) == 0
}
func (q *OfflineQueue) loadFromStorage() {
data, err := os.ReadFile(q.storagePath)
if err != nil {
return
}
var queue []NotificationPayload
if err := json.Unmarshal(data, &queue); err == nil {
q.queue = queue
}
}
func (q *OfflineQueue) saveToStorage() {
dir := filepath.Dir(q.storagePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return
}
data, err := json.Marshal(q.queue)
if err != nil {
return
}
_ = os.WriteFile(q.storagePath, data, 0644)
}

222
transport.go Normal file
View File

@ -0,0 +1,222 @@
package ironnotify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
// Transport handles HTTP communication with the IronNotify API.
type Transport struct {
baseURL string
apiKey string
debug bool
httpClient *http.Client
}
// NewTransport creates a new Transport.
func NewTransport(baseURL, apiKey string, timeout time.Duration, debug bool) *Transport {
return &Transport{
baseURL: baseURL,
apiKey: apiKey,
debug: debug,
httpClient: &http.Client{
Timeout: timeout,
},
}
}
// Send sends a notification payload to the server.
func (t *Transport) Send(ctx context.Context, payload *NotificationPayload) SendResult {
data, err := json.Marshal(payload)
if err != nil {
return SendResult{Success: false, Error: err.Error()}
}
req, err := http.NewRequestWithContext(ctx, "POST", t.baseURL+"/api/v1/notify", bytes.NewReader(data))
if err != nil {
return SendResult{Success: false, Error: err.Error()}
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+t.apiKey)
if t.debug {
fmt.Printf("[IronNotify] Sending notification: %s\n", payload.EventType)
}
resp, err := t.httpClient.Do(req)
if err != nil {
return SendResult{Success: false, Error: err.Error()}
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
var result struct {
NotificationID string `json:"notificationId"`
}
if err := json.Unmarshal(body, &result); err == nil {
return SendResult{Success: true, NotificationID: result.NotificationID}
}
return SendResult{Success: true}
}
var errorResp struct {
Error string `json:"error"`
}
if err := json.Unmarshal(body, &errorResp); err == nil && errorResp.Error != "" {
return SendResult{Success: false, Error: errorResp.Error}
}
return SendResult{Success: false, Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(body))}
}
// GetNotifications retrieves notifications from the server.
func (t *Transport) GetNotifications(ctx context.Context, limit, offset int, unreadOnly bool) ([]Notification, error) {
u, err := url.Parse(t.baseURL + "/api/v1/notifications")
if err != nil {
return nil, err
}
q := u.Query()
if limit > 0 {
q.Set("limit", strconv.Itoa(limit))
}
if offset > 0 {
q.Set("offset", strconv.Itoa(offset))
}
if unreadOnly {
q.Set("unread_only", "true")
}
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+t.apiKey)
resp, err := t.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var notifications []Notification
if err := json.NewDecoder(resp.Body).Decode(&notifications); err != nil {
return nil, err
}
return notifications, nil
}
// GetUnreadCount retrieves the unread notification count.
func (t *Transport) GetUnreadCount(ctx context.Context) (int, error) {
req, err := http.NewRequestWithContext(ctx, "GET", t.baseURL+"/api/v1/notifications/unread-count", nil)
if err != nil {
return 0, err
}
req.Header.Set("Authorization", "Bearer "+t.apiKey)
resp, err := t.httpClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var result struct {
Count int `json:"count"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0, err
}
return result.Count, nil
}
// MarkAsRead marks a notification as read.
func (t *Transport) MarkAsRead(ctx context.Context, notificationID string) error {
req, err := http.NewRequestWithContext(ctx, "POST", t.baseURL+"/api/v1/notifications/"+notificationID+"/read", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+t.apiKey)
resp, err := t.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
// MarkAllAsRead marks all notifications as read.
func (t *Transport) MarkAllAsRead(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "POST", t.baseURL+"/api/v1/notifications/read-all", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+t.apiKey)
resp, err := t.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
// IsOnline checks if the API is reachable.
func (t *Transport) IsOnline(ctx context.Context) bool {
req, err := http.NewRequestWithContext(ctx, "GET", t.baseURL+"/health", nil)
if err != nil {
return false
}
resp, err := t.httpClient.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
// Close closes the transport.
func (t *Transport) Close() {
t.httpClient.CloseIdleConnections()
}

80
types.go Normal file
View File

@ -0,0 +1,80 @@
// Package ironnotify provides an SDK for sending notifications via IronNotify.
package ironnotify
import "time"
// SeverityLevel represents the severity of a notification.
type SeverityLevel string
const (
SeverityInfo SeverityLevel = "info"
SeveritySuccess SeverityLevel = "success"
SeverityWarning SeverityLevel = "warning"
SeverityError SeverityLevel = "error"
SeverityCritical SeverityLevel = "critical"
)
// ConnectionState represents the WebSocket connection state.
type ConnectionState string
const (
StateDisconnected ConnectionState = "disconnected"
StateConnecting ConnectionState = "connecting"
StateConnected ConnectionState = "connected"
StateReconnecting ConnectionState = "reconnecting"
)
// NotificationAction represents an action button on a notification.
type NotificationAction struct {
Label string `json:"label"`
URL string `json:"url,omitempty"`
Action string `json:"action,omitempty"`
Style string `json:"style,omitempty"`
}
// NotificationPayload represents the data sent to create a notification.
type NotificationPayload struct {
EventType string `json:"eventType"`
Title string `json:"title"`
Message string `json:"message,omitempty"`
Severity SeverityLevel `json:"severity,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Actions []NotificationAction `json:"actions,omitempty"`
UserID string `json:"userId,omitempty"`
GroupKey string `json:"groupKey,omitempty"`
DeduplicationKey string `json:"deduplicationKey,omitempty"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
}
// Notification represents a notification received from the server.
type Notification struct {
ID string `json:"id"`
EventType string `json:"eventType"`
Title string `json:"title"`
Message string `json:"message,omitempty"`
Severity SeverityLevel `json:"severity"`
Metadata map[string]any `json:"metadata,omitempty"`
Actions []NotificationAction `json:"actions,omitempty"`
UserID string `json:"userId,omitempty"`
GroupKey string `json:"groupKey,omitempty"`
Read bool `json:"read"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
}
// SendResult represents the result of sending a notification.
type SendResult struct {
Success bool `json:"success"`
NotificationID string `json:"notificationId,omitempty"`
Error string `json:"error,omitempty"`
Queued bool `json:"queued,omitempty"`
}
// NotificationHandler is a callback function for receiving notifications.
type NotificationHandler func(notification Notification)
// UnreadCountHandler is a callback function for unread count changes.
type UnreadCountHandler func(count int)
// ConnectionStateHandler is a callback function for connection state changes.
type ConnectionStateHandler func(state ConnectionState)