mirror of
https://github.com/hanzoai/notify.git
synced 2026-10-30 15:04:32 +00:00
Replaces the chi-based notifyd scaffold with the canonical
hanzoai/auto-style layout: hanzoai/base as the HTTP + SQLite + auth
substrate (via the platform plugin for IAM JWT validation and
X-Org-Id injection), hanzoai/tasks for durable async sends, and a
per-tenant Notifier resolver that pulls provider credentials from
KMS through platform.KMSClient.
Library at the module root stays untouched — all 33 provider impls
(plivo, twilio, ses, sendgrid, mail, whatsapp, discord, slack, …)
keep their existing public API. The service wraps them.
cmd/notifyd
- base.New + platform.MustRegister
- schema.MustRegister (6 collections: tenants, providers,
templates, events, messages, meter)
- tasks worker registered when TASKS_ADDR is set; sync path
runs the same Deliver activity directly when not
internal/
- boot/ thin platform.KMSClient wiring (env-driven)
- schema/ 6 base collections
- routes/ /v1/notify/{send,messages,providers,templates,events,metering,health}
+ convenience /send/{sms,email,voice,whatsapp}
- tenant/ KMS-backed resolver builds library Notifier per send
- template/ draft → pending → approved → published → archived
- event/ catalog routing (fills missing channel / template)
- metering/ per-send ledger writer + aggregate query
- tasks/ NotifySendWorkflow + Deliver activity (idempotent on
replay; reloads message row before resending)
- zaprpc/ procedure name constants (server impl follow-up)
pkg/
- client/ public HTTP SDK (Send / Message / Providers / Templates)
- types/ wire shapes shared by routes and client
cucloud: fmt.Errorf(msg) → errors.New(msg) for the go 1.26 vet pass
that landed with this dependency bump. Behaviour unchanged.
Boot smoke-tested locally:
- SQLite migrations apply cleanly
- /v1/notify/health returns {"service":"notify","status":"ok"}
- missing X-Org-Id returns 401 on every authenticated route
- send with stub tenant returns the expected base validation error
160 lines
3.6 KiB
Go
160 lines
3.6 KiB
Go
// Copyright 2025 The Hanzo IAM Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cucloud
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
type CuCloud struct {
|
|
AccessKey string
|
|
SecretKey string
|
|
TopicName string
|
|
MessageTitle string
|
|
CloudRegionCode string
|
|
AccountId string
|
|
NotifyType string
|
|
Client http.Client
|
|
}
|
|
|
|
type CuCloudResp struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Result string `json:"result"`
|
|
}
|
|
|
|
func New(accessKey, secretKey, topicName, messageTitle, cloudRegionCode, accountId, notifyType string) *CuCloud {
|
|
return &CuCloud{
|
|
accessKey,
|
|
secretKey,
|
|
topicName,
|
|
messageTitle,
|
|
cloudRegionCode,
|
|
accountId,
|
|
notifyType,
|
|
http.Client{},
|
|
}
|
|
}
|
|
|
|
func (c *CuCloud) Send(ctx context.Context, subject, content string) error {
|
|
timeNow := time.Now().UnixMilli()
|
|
|
|
reqHeader := map[string]string{}
|
|
reqHeader["algorithm"] = "HmacSHA256"
|
|
reqHeader["requestTime"] = strconv.FormatInt(timeNow, 10)
|
|
reqHeader["accessKey"] = c.AccessKey
|
|
|
|
reqBody := map[string]string{}
|
|
|
|
reqBody["notifyType"] = c.NotifyType
|
|
reqBody["messageTitle"] = url.QueryEscape(c.MessageTitle)
|
|
reqBody["messageType"] = "text"
|
|
reqBody["messageContent"] = url.QueryEscape(content)
|
|
reqBody["topicName"] = url.QueryEscape(c.TopicName)
|
|
reqBody["messageTag"] = ""
|
|
reqBody["templateName"] = ""
|
|
reqBody["cloudRegionCode"] = "cn-langfang-2"
|
|
|
|
signVal, err := c.generateRequestSign(reqHeader, reqBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reqHeader["sign"] = signVal
|
|
reqHeader["Content-Type"] = "application/json"
|
|
reqHeader["Account-Id"] = c.AccountId
|
|
reqHeader["User-Id"] = c.AccountId
|
|
reqHeader["Region-Code"] = c.CloudRegionCode
|
|
|
|
bodyJson, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest("POST", "https://gateway.cucloud.cn/smn/SMNService/api/message/notify", bytes.NewReader(bodyJson))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k, v := range reqHeader {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBodyRaw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var respBody CuCloudResp
|
|
|
|
err = json.Unmarshal(respBodyRaw, &respBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if respBody.Code != 200 {
|
|
return errors.New(respBody.Message)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *CuCloud) generateRequestSign(header map[string]string, body map[string]string) (string, error) {
|
|
mac := hmac.New(sha256.New, []byte(c.SecretKey))
|
|
reqSignMap := make(map[string]string)
|
|
maps.Copy(reqSignMap, header)
|
|
maps.Copy(reqSignMap, body)
|
|
signRawString := ""
|
|
|
|
var keys []string
|
|
for k, _ := range reqSignMap {
|
|
keys = append(keys, k)
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
for _, k := range keys {
|
|
vJson, err := json.Marshal(reqSignMap[k])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
signRawString += k + "=" + string(vJson) + "&"
|
|
}
|
|
|
|
signRawString = signRawString[:len(signRawString)-1]
|
|
mac.Write([]byte(signRawString))
|
|
return hex.EncodeToString(mac.Sum(nil)), nil
|
|
}
|