Files
flatrender/services/render/internal/notifier/notifier.go
T

136 lines
4.3 KiB
Go
Raw Normal View History

// Package notifier provides a lightweight HTTP client for the notification
// service. All methods are fire-and-forget: errors are logged but never
// propagate to the caller, so a notification failure never breaks a render
// flow.
package notifier
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/google/uuid"
)
// Client sends notifications to the internal notification service endpoint.
type Client struct {
baseURL string
serviceToken string
http *http.Client
}
// New returns a Client pointing at baseURL (e.g. "http://notification-svc:8080")
// and authenticating with serviceToken.
func New(baseURL, serviceToken string) *Client {
return &Client{
baseURL: baseURL,
serviceToken: serviceToken,
http: &http.Client{Timeout: 5 * time.Second},
}
}
// createNotificationReq mirrors the notification service's CreateNotificationRequest.
type createNotificationReq struct {
UserID uuid.UUID `json:"user_id"`
TenantID uuid.UUID `json:"tenant_id"`
NotificationType string `json:"notification_type"`
Priority string `json:"priority"`
Title string `json:"title"`
Message string `json:"message"`
RenderJobID *uuid.UUID `json:"render_job_id,omitempty"`
ExportID *uuid.UUID `json:"export_id,omitempty"`
ActionURL *string `json:"action_url,omitempty"`
ActionText *string `json:"action_text,omitempty"`
Channels []string `json:"channels"`
}
// send posts a notification to the service. Returns an error for caller
// awareness, but callers are expected to ignore it.
func (c *Client) send(ctx context.Context, req createNotificationReq) error {
body, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("notifier marshal: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost,
c.baseURL+"/v1/internal/notifications", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("notifier new request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+c.serviceToken)
resp, err := c.http.Do(httpReq)
if err != nil {
return fmt.Errorf("notifier send: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("notifier: status %d", resp.StatusCode)
}
return nil
}
// NotifyRenderDone fires a RenderCompleted notification. Never blocks the
// caller — errors are only logged.
func (c *Client) NotifyRenderDone(
ctx context.Context,
userID, tenantID, jobID uuid.UUID,
exportID *uuid.UUID,
jobName string,
) {
actionURL := "/dashboard/renders/" + jobID.String()
actionText := "مشاهده فایل"
req := createNotificationReq{
UserID: userID,
TenantID: tenantID,
NotificationType: "RenderCompleted",
Priority: "High",
Title: "رندر تکمیل شد 🎉",
Message: fmt.Sprintf("پروژه «%s» با موفقیت رندر شد و آماده دانلود است.", jobName),
RenderJobID: &jobID,
ExportID: exportID,
ActionURL: &actionURL,
ActionText: &actionText,
Channels: []string{"InApp"},
}
if err := c.send(ctx, req); err != nil {
log.Printf("[notifier] RenderDone job=%s err=%v", jobID, err)
}
}
// NotifyRenderFailed fires a RenderFailed notification. Never blocks the
// caller — errors are only logged.
func (c *Client) NotifyRenderFailed(
ctx context.Context,
userID, tenantID, jobID uuid.UUID,
jobName, reason string,
) {
actionURL := "/dashboard/renders/" + jobID.String()
actionText := "جزئیات"
var msg string
if reason != "" {
msg = fmt.Sprintf("رندر پروژه «%s» با خطا مواجه شد: %s", jobName, reason)
} else {
msg = fmt.Sprintf("رندر پروژه «%s» با خطا مواجه شد. لطفاً دوباره تلاش کنید.", jobName)
}
req := createNotificationReq{
UserID: userID,
TenantID: tenantID,
NotificationType: "RenderFailed",
Priority: "High",
Title: "خطا در رندر",
Message: msg,
RenderJobID: &jobID,
ActionURL: &actionURL,
ActionText: &actionText,
Channels: []string{"InApp"},
}
if err := c.send(ctx, req); err != nil {
log.Printf("[notifier] RenderFailed job=%s err=%v", jobID, err)
}
}