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:
parent
7ce102c947
commit
961d963feb
|
|
@ -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
293
README.md
|
|
@ -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.
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/github.com/IronServices/ironnotify-go)
|
||||||
|
[](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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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(), ¬ifications[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, ¬ifications[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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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(¬ifications); 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()
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue