diff --git a/common/config/config.go b/common/config/config.go index 203b37891..908521db6 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -424,38 +424,47 @@ type Config struct { } Notification struct { - Port int `env:"STARHUB_SERVER_NOTIFIER_PORT" default:"8095"` - Host string `env:"STARHUB_SERVER_NOTIFIER_HOST" default:"http://localhost"` - MailerHost string `env:"STARHUB_SERVER_MAILER_HOST" default:"smtp.qiye.aliyun.com"` - MailerPort int `env:"STARHUB_SERVER_MAILER_PORT" default:"465"` - MailerUsername string `env:"STARHUB_SERVER_MAILER_USERNAME" default:""` - MailerPassword string `env:"STARHUB_SERVER_MAILER_PASSWORD" default:""` - DirectMailEnabled bool `env:"STARHUB_SERVER_DIRECT_MAIL_ENABLED" default:"false"` - DirectMailAccessKeyID string `env:"STARHUB_SERVER_DIRECT_MAIL_ACCESS_KEY_ID" default:""` - DirectMailAccessKeySecret string `env:"STARHUB_SERVER_DIRECT_MAIL_ACCESS_KEY_SECRET" default:""` - DirectMailEndpoint string `env:"STARHUB_SERVER_DIRECT_MAIL_ENDPOINT" default:"dm.aliyuncs.com"` - DirectMailRegionId string `env:"STARHUB_SERVER_DIRECT_MAIL_REGION_ID" default:"cn-hangzhou"` - MailerRechargeAdmin string `env:"STARHUB_SERVER_MAILER_RECHARGE_ADMIN" default:"contact@opencsg.com"` - MailerWeeklyRechargesMail string `env:"STARHUB_SERVER_MAILER_WEEKLY_RECHARGES_MAIL" default:"reconcile@opencsg.com"` - EmailInvoiceCreatedReceiver string `env:"STARHUB_SERVER_EMAIL_INVOICE_CREATED_RECEIVER" default:"contact@opencsg.com"` - RepoSyncTimezone string `env:"STARHUB_SERVER_REPO_SYNC_TIMEZONE" default:"Asia/Shanghai"` - RepoSyncChatID string `env:"STARHUB_SERVER_REPO_SYNC_CHAT_ID" default:""` - NotificationRetryCount int `env:"STARHUB_SERVER_NOTIFIER_NOTIFICATION_RETRY_COUNT" default:"3"` - BroadcastUserPageSize int `env:"STARHUB_SERVER_NOTIFIER_BROADCAST_USER_PAGE_SIZE" default:"100"` - BroadcastEmailPageSize int `env:"STARHUB_SERVER_NOTIFIER_BROADCAST_EMAIL_PAGE_SIZE" default:"100"` - MsgDispatcherCount int `env:"STARHUB_SERVER_NOTIFIER_MSG_DISPATCHER_COUNT" default:"20"` - HighPriorityMsgBufferSize int `env:"STARHUB_SERVER_NOTIFIER_HIGH_PRIORITY_MSG_BUFFER_SIZE" default:"100"` - NormalPriorityMsgBufferSize int `env:"STARHUB_SERVER_NOTIFIER_NORMAL_PRIORITY_MSG_BUFFER_SIZE" default:"50"` - HighPriorityMsgAckWait int `env:"STARHUB_SERVER_NOTIFIER_HIGH_PRIORITY_MSG_ACK_WAIT" default:"60"` - NormalPriorityMsgAckWait int `env:"STARHUB_SERVER_NOTIFIER_NORMAL_PRIORITY_MSG_ACK_WAIT" default:"60"` - HighPriorityMsgMaxDeliver int `env:"STARHUB_SERVER_NOTIFIER_HIGH_PRIORITY_MSG_MAX_DELIVER" default:"6"` - NormalPriorityMsgMaxDeliver int `env:"STARHUB_SERVER_NOTIFIER_NORMAL_PRIORITY_MSG_MAX_DELIVER" default:"6"` - DeduplicateWindow int `env:"STARHUB_SERVER_NOTIFIER_DEDUPLICATE_WINDOW" default:"5"` // 5 seconds + Port int `env:"STARHUB_SERVER_NOTIFIER_PORT" default:"8095"` + Host string `env:"STARHUB_SERVER_NOTIFIER_HOST" default:"http://localhost"` + MailerHost string `env:"STARHUB_SERVER_MAILER_HOST" default:"smtp.qiye.aliyun.com"` + MailerPort int `env:"STARHUB_SERVER_MAILER_PORT" default:"465"` + MailerUsername string `env:"STARHUB_SERVER_MAILER_USERNAME" default:""` + MailerPassword string `env:"STARHUB_SERVER_MAILER_PASSWORD" default:""` + DirectMailEnabled bool `env:"STARHUB_SERVER_DIRECT_MAIL_ENABLED" default:"false"` + DirectMailAccessKeyID string `env:"STARHUB_SERVER_DIRECT_MAIL_ACCESS_KEY_ID" default:""` + DirectMailAccessKeySecret string `env:"STARHUB_SERVER_DIRECT_MAIL_ACCESS_KEY_SECRET" default:""` + DirectMailEndpoint string `env:"STARHUB_SERVER_DIRECT_MAIL_ENDPOINT" default:"dm.aliyuncs.com"` + DirectMailRegionId string `env:"STARHUB_SERVER_DIRECT_MAIL_REGION_ID" default:"cn-hangzhou"` + MailerRechargeAdmin string `env:"STARHUB_SERVER_MAILER_RECHARGE_ADMIN" default:"contact@opencsg.com"` + MailerWeeklyRechargesMail string `env:"STARHUB_SERVER_MAILER_WEEKLY_RECHARGES_MAIL" default:"reconcile@opencsg.com"` + EmailInvoiceCreatedReceiver string `env:"STARHUB_SERVER_EMAIL_INVOICE_CREATED_RECEIVER" default:"contact@opencsg.com"` + RepoSyncTimezone string `env:"STARHUB_SERVER_REPO_SYNC_TIMEZONE" default:"Asia/Shanghai"` + RepoSyncChatID string `env:"STARHUB_SERVER_REPO_SYNC_CHAT_ID" default:""` + NotificationRetryCount int `env:"STARHUB_SERVER_NOTIFIER_NOTIFICATION_RETRY_COUNT" default:"3"` + BroadcastUserPageSize int `env:"STARHUB_SERVER_NOTIFIER_BROADCAST_USER_PAGE_SIZE" default:"100"` + BroadcastEmailPageSize int `env:"STARHUB_SERVER_NOTIFIER_BROADCAST_EMAIL_PAGE_SIZE" default:"100"` + MsgDispatcherCount int `env:"STARHUB_SERVER_NOTIFIER_MSG_DISPATCHER_COUNT" default:"20"` + HighPriorityMsgBufferSize int `env:"STARHUB_SERVER_NOTIFIER_HIGH_PRIORITY_MSG_BUFFER_SIZE" default:"100"` + NormalPriorityMsgBufferSize int `env:"STARHUB_SERVER_NOTIFIER_NORMAL_PRIORITY_MSG_BUFFER_SIZE" default:"50"` + HighPriorityMsgAckWait int `env:"STARHUB_SERVER_NOTIFIER_HIGH_PRIORITY_MSG_ACK_WAIT" default:"60"` + NormalPriorityMsgAckWait int `env:"STARHUB_SERVER_NOTIFIER_NORMAL_PRIORITY_MSG_ACK_WAIT" default:"60"` + HighPriorityMsgMaxDeliver int `env:"STARHUB_SERVER_NOTIFIER_HIGH_PRIORITY_MSG_MAX_DELIVER" default:"6"` + NormalPriorityMsgMaxDeliver int `env:"STARHUB_SERVER_NOTIFIER_NORMAL_PRIORITY_MSG_MAX_DELIVER" default:"6"` + DeduplicateWindow int `env:"STARHUB_SERVER_NOTIFIER_DEDUPLICATE_WINDOW" default:"5"` // 5 seconds + + // SMS Provider Configuration + SMSProvider string `env:"STARHUB_SERVER_NOTIFIER_SMS_PROVIDER" default:"aliyun"` // aliyun, tencent, huawei + SMSSign string `env:"STARHUB_SERVER_NOTIFIER_SMS_SIGN" default:""` - SMSAccessKeyID string `env:"STARHUB_SERVER_NOTIFIER_SMS_ACCESS_KEY_ID" default:""` - SMSAccessKeySecret string `env:"STARHUB_SERVER_NOTIFIER_SMS_ACCESS_KEY_SECRET" default:""` SMSTemplateCodeForVerifyCodeCN string `env:"STARHUB_SERVER_NOTIFIER_SMS_TEMPLATE_CODE_FOR_VERIFY_CODE_CN" default:""` SMSTemplateCodeForVerifyCodeOversea string `env:"STARHUB_SERVER_NOTIFIER_SMS_TEMPLATE_CODE_FOR_VERIFY_CODE_OVERSEA" default:""` + + // Alibaba Cloud SMS Configuration (Backward Compatibility) + SMSAccessKeyID string `env:"STARHUB_SERVER_NOTIFIER_SMS_ACCESS_KEY_ID"` + SMSAccessKeySecret string `env:"STARHUB_SERVER_NOTIFIER_SMS_ACCESS_KEY_SECRET"` + SMSRegion string `env:"STARHUB_SERVER_NOTIFIER_SMS_REGION" default:"ap-guangzhou"` + SMSEndpoint string `env:"STARHUB_SERVER_NOTIFIER_SMS_ENDPOINT"` + SMSAppID string `env:"STARHUB_SERVER_NOTIFIER_SMS_APP_ID"` // Tencent SdkAppId / Huawei ProjectId } Prometheus struct { @@ -623,6 +632,7 @@ func loadConfig() (*Config, error) { if len(cfg.UniqueServiceName) < 1 { cfg.UniqueServiceName = genServiceName() } + return cfg, err } diff --git a/common/types/notification.go b/common/types/notification.go index 53ea72900..972822a98 100644 --- a/common/types/notification.go +++ b/common/types/notification.go @@ -322,8 +322,10 @@ type EmailWeeklyRechargesNotification struct { } type SMSReq struct { - PhoneNumbers []string `json:"phone_numbers"` - SignName string `json:"sign_name"` - TemplateCode string `json:"template_code"` - TemplateParam string `json:"template_param"` + PhoneNumbers []string `json:"phone_numbers"` + SignName string `json:"sign_name"` + TemplateCode string `json:"template_code"` + + Params []string `json:"params"` + MapParams map[string]string `json:"map_params"` } diff --git a/go.mod b/go.mod index fda50f051..e947e0a30 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/golang/mock v1.7.0-rc.1 github.com/google/wire v0.6.0 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.190 github.com/jarcoal/httpmock v1.3.1 github.com/larksuite/oapi-sdk-go/v3 v3.4.18 github.com/looplab/fsm v1.0.3 @@ -61,6 +62,8 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.2 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.69 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.57 github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 github.com/tidwall/sjson v1.2.5 @@ -174,6 +177,7 @@ require ( github.com/go-pay/xlog v0.0.3 // indirect github.com/go-pay/xtime v0.0.2 // indirect github.com/go-sql-driver/mysql v1.9.1 // indirect + github.com/goccy/go-yaml v1.9.8 // indirect github.com/gocql/gocql v1.7.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -267,6 +271,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect gitlab.com/gitlab-org/go/reopen v1.0.0 // indirect gitlab.com/gitlab-org/labkit v1.21.2 // indirect + go.mongodb.org/mongo-driver v1.13.1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect @@ -352,7 +357,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect + github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index a1c81f548..10eee3739 100644 --- a/go.sum +++ b/go.sum @@ -1249,6 +1249,7 @@ github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/X github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -1444,6 +1445,8 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ= +github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= @@ -1517,6 +1520,7 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -1711,6 +1715,8 @@ github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFD github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.190 h1:PZ4FlHVULGjP6dnqjDAM3YDiqtZ2pP9XEZzkRAX1Q/E= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.190/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= @@ -1770,8 +1776,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU= +github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -1863,6 +1870,7 @@ github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -1967,6 +1975,7 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= @@ -2283,6 +2292,7 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.2-0.20170726183946-abee6f9b0679/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= @@ -2395,6 +2405,11 @@ github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb/go.mod h1:143 github.com/temporalio/tchannel-go v1.22.1-0.20220818200552-1be8d8cffa5b/go.mod h1:c+V9Z/ZgkzAdyGvHrvC5AsXgN+M9Qwey04cBdKYzV7U= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 h1:sEJGhmDo+0FaPWM6f0v8Tjia0H5pR6/Baj6+kS78B+M= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938/go.mod h1:ezRQRwu9KQXy8Wuuv1aaFFxoCNz5CeNbVOOkh3xctbY= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.57/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.69 h1:WntotnV2HpQhYXOGZQ5fGk2AbY4MPkXQcd/GtHBdubI= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.69/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.57 h1:ZnJK+aTZYyzGN/4dmQXYWzuHsuZFrlj034uLoGaNVvQ= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.3.57/go.mod h1:jwLLFaeXXAnkWj37iTh0jfeXDYWf9eggaKJ1dRnc/1A= github.com/testcontainers/testcontainers-go v0.26.0/go.mod h1:ICriE9bLX5CLxL9OFQ2N+2N+f+803LNJ1utJb1+Inx0= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= @@ -2494,6 +2509,9 @@ github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= @@ -2504,6 +2522,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -2547,6 +2566,8 @@ go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGy go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= go.etcd.io/etcd/server/v3 v3.5.5/go.mod h1:rZ95vDw/jrvsbj9XpTqPrTAB9/kzchVdhRirySPkUBc= go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= +go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= +go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -2746,6 +2767,7 @@ golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -3102,6 +3124,7 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/notification/notifychannel/channel/sms/client/aliyun_sms_client.go b/notification/notifychannel/channel/sms/client/aliyun_sms_client.go new file mode 100644 index 000000000..e2031a9e5 --- /dev/null +++ b/notification/notifychannel/channel/sms/client/aliyun_sms_client.go @@ -0,0 +1,85 @@ +package client + +import ( + "encoding/json" + "fmt" + "log/slog" + "strings" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v5/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + "opencsg.com/csghub-server/common/config" + "opencsg.com/csghub-server/common/types" +) + +type SMSClient interface { + SendSmsWithOptions( + request *dysmsapi20170525.SendSmsRequest, + runtime *util.RuntimeOptions, + ) (*dysmsapi20170525.SendSmsResponse, error) +} + +type AliyunSMSClient struct { + client SMSClient +} + +var _ SMSService = (*AliyunSMSClient)(nil) + +func NewAliyunSMSClient(config *config.Config) (SMSService, error) { + // Use new config fields, but maintain backward compatibility + accessKeyID := config.Notification.SMSAccessKeyID + accessKeySecret := config.Notification.SMSAccessKeySecret + + // If new config fields are empty, try to use old config fields (compatibility handling) + if accessKeyID == "" { + accessKeyID = config.Notification.SMSAccessKeyID + } + if accessKeySecret == "" { + accessKeySecret = config.Notification.SMSAccessKeySecret + } + + if accessKeyID == "" || accessKeySecret == "" { + return nil, fmt.Errorf("Aliyun SMS configuration incomplete, please set ALIYUN_SMS_ACCESS_KEY_ID and ALIYUN_SMS_ACCESS_KEY_SECRET") + } + + SMSConfig := &openapi.Config{ + AccessKeyId: tea.String(accessKeyID), + AccessKeySecret: tea.String(accessKeySecret), + Endpoint: tea.String(config.Notification.SMSEndpoint), + } + client, err := dysmsapi20170525.NewClient(SMSConfig) + if err != nil { + return nil, err + } + return &AliyunSMSClient{ + client: client, + }, nil +} + +func (c *AliyunSMSClient) Send(req types.SMSReq) error { + // refer to sms client doc, the phone area should not have '+' prefix when send sms code to overseas, + for i, phoneNumber := range req.PhoneNumbers { + req.PhoneNumbers[i] = strings.TrimPrefix(phoneNumber, "+") + } + phoneNumbers := strings.Join(req.PhoneNumbers, ",") + + templateParam, err := json.Marshal(req.MapParams) + if err != nil { + slog.Error("Failed to marshal map params to JSON", slog.Any("error", err)) + return err + } + smsReq := &dysmsapi20170525.SendSmsRequest{ + PhoneNumbers: tea.String(phoneNumbers), + SignName: tea.String(req.SignName), + TemplateCode: tea.String(req.TemplateCode), + TemplateParam: tea.String(string(templateParam)), + } + + _, err = c.client.SendSmsWithOptions(smsReq, &util.RuntimeOptions{}) + if err != nil { + return err + } + return nil +} diff --git a/notification/notifychannel/channel/sms/client/aliyun_sms_client_test.go b/notification/notifychannel/channel/sms/client/aliyun_sms_client_test.go new file mode 100644 index 000000000..d00b39e11 --- /dev/null +++ b/notification/notifychannel/channel/sms/client/aliyun_sms_client_test.go @@ -0,0 +1,73 @@ +package client + +import ( + "errors" + "testing" + + dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v5/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "opencsg.com/csghub-server/common/types" +) + +type mockSMSClient struct { + sendFunc func(req *dysmsapi20170525.SendSmsRequest, runtime *util.RuntimeOptions) (*dysmsapi20170525.SendSmsResponse, error) +} + +func (m *mockSMSClient) SendSmsWithOptions( + req *dysmsapi20170525.SendSmsRequest, + runtime *util.RuntimeOptions, +) (*dysmsapi20170525.SendSmsResponse, error) { + return m.sendFunc(req, runtime) +} + +func TestAliyunSMSClient_Send_Success(t *testing.T) { + mock := &mockSMSClient{ + sendFunc: func(req *dysmsapi20170525.SendSmsRequest, runtime *util.RuntimeOptions) (*dysmsapi20170525.SendSmsResponse, error) { + if *req.PhoneNumbers != "1234567890" { + t.Errorf("unexpected phone number: %s", *req.PhoneNumbers) + } + if *req.SignName != "TestSign" { + t.Errorf("unexpected sign name: %s", *req.SignName) + } + return &dysmsapi20170525.SendSmsResponse{}, nil + }, + } + + client := &AliyunSMSClient{client: mock} + req := types.SMSReq{ + PhoneNumbers: []string{"1234567890"}, + SignName: "TestSign", + TemplateCode: "TEMPLATE_001", + MapParams: map[string]string{ + "code": "1234", + }, + } + + err := client.Send(req) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestAliyunSMSClient_Send_Failure(t *testing.T) { + mock := &mockSMSClient{ + sendFunc: func(req *dysmsapi20170525.SendSmsRequest, runtime *util.RuntimeOptions) (*dysmsapi20170525.SendSmsResponse, error) { + return nil, errors.New("send failed") + }, + } + + client := &AliyunSMSClient{client: mock} + req := types.SMSReq{ + PhoneNumbers: []string{"1234567890"}, + SignName: "TestSign", + TemplateCode: "TEMPLATE_001", + MapParams: map[string]string{ + "code": "1234", + }, + } + + err := client.Send(req) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/notification/notifychannel/channel/sms/client/factory/factory.go b/notification/notifychannel/channel/sms/client/factory/factory.go new file mode 100644 index 000000000..e3ab3068f --- /dev/null +++ b/notification/notifychannel/channel/sms/client/factory/factory.go @@ -0,0 +1,46 @@ +package factory + +import ( + "log/slog" + + "opencsg.com/csghub-server/common/config" + "opencsg.com/csghub-server/notification/notifychannel/channel/sms/client" +) + +// ProviderType SMS provider type +type ProviderType string + +const ( + ProviderAliyun ProviderType = "aliyun" + ProviderTencent ProviderType = "tencent" + ProviderHuawei ProviderType = "huawei" +) + +type SMSFactory interface { + CreateSMSClient(config *config.Config) (client.SMSService, error) +} + +type DefaultSMSFactory struct{} + +func NewDefaultSMSFactory() SMSFactory { + return &DefaultSMSFactory{} +} + +func (f *DefaultSMSFactory) CreateSMSClient(config *config.Config) (client.SMSService, error) { + provider := ProviderType(config.Notification.SMSProvider) + + switch provider { + case ProviderAliyun: + return createAliyunSMSClient(config) + default: + slog.Warn("Unknown SMS provider, using default Aliyun", slog.String("provider", string(provider))) + return createAliyunSMSClient(config) + } +} + +// createAliyunSMSClient creates Aliyun SMS client +func createAliyunSMSClient(config *config.Config) (client.SMSService, error) { + // Call the existing NewAliyunSMSClient function + // Note: Need to update the existing NewAliyunSMSClient function to use new config fields + return client.NewAliyunSMSClient(config) +} diff --git a/notification/notifychannel/channel/sms/client/factory/factory_test.go b/notification/notifychannel/channel/sms/client/factory/factory_test.go new file mode 100644 index 000000000..0fe5eb8d1 --- /dev/null +++ b/notification/notifychannel/channel/sms/client/factory/factory_test.go @@ -0,0 +1,83 @@ +package factory + +import ( + "testing" + + "opencsg.com/csghub-server/common/config" +) + +func TestDefaultSMSFactory_CreateSMSClient(t *testing.T) { + tests := []struct { + name string + provider string + wantErr bool + }{ + { + name: "Create Aliyun client", + provider: "aliyun", + wantErr: false, // May error with incomplete config, but factory will create + }, + { + name: "Create Tencent Cloud client", + provider: "tencent", + wantErr: false, // May error with incomplete config, but factory will create + }, + { + name: "Create Huawei Cloud client", + provider: "huawei", + wantErr: false, // May error with incomplete config, but factory will create + }, + { + name: "Unknown provider falls back to Aliyun", + provider: "unknown", + wantErr: false, // Will fall back to Aliyun + }, + } + + factory := NewDefaultSMSFactory() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &config.Config{} + cfg.Notification.SMSProvider = tt.provider + + // Set some test configurations + cfg.Notification.SMSAccessKeyID = "test-ak" + cfg.Notification.SMSAccessKeySecret = "test-sk" + cfg.Notification.SMSAppID = "test-tencent-id" + cfg.Notification.SMSEndpoint = "test-tencent-endpoint" + cfg.Notification.SMSRegion = "test-region" + + client, err := factory.CreateSMSClient(cfg) + + if (err != nil) != tt.wantErr { + t.Errorf("CreateSMSClient() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if client == nil && !tt.wantErr { + t.Error("CreateSMSClient() returned nil client but no error") + } + }) + } +} + +func TestProviderTypeConstants(t *testing.T) { + tests := []struct { + name string + provider ProviderType + expected string + }{ + {"Aliyun", ProviderAliyun, "aliyun"}, + {"Tencent Cloud", ProviderTencent, "tencent"}, + {"Huawei Cloud", ProviderHuawei, "huawei"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if string(tt.provider) != tt.expected { + t.Errorf("ProviderType %v = %v, want %v", tt.name, tt.provider, tt.expected) + } + }) + } +} diff --git a/notification/notifychannel/channel/sms/client/sms_service.go b/notification/notifychannel/channel/sms/client/sms_service.go new file mode 100644 index 000000000..389f44d45 --- /dev/null +++ b/notification/notifychannel/channel/sms/client/sms_service.go @@ -0,0 +1,7 @@ +package client + +import "opencsg.com/csghub-server/common/types" + +type SMSService interface { + Send(req types.SMSReq) error +} diff --git a/notification/notifychannel/channel/sms/sms.go b/notification/notifychannel/channel/sms/sms.go new file mode 100644 index 000000000..31d95a8e0 --- /dev/null +++ b/notification/notifychannel/channel/sms/sms.go @@ -0,0 +1,50 @@ +package sms + +import ( + "context" + "fmt" + "log/slog" + + "opencsg.com/csghub-server/common/types" + "opencsg.com/csghub-server/notification/notifychannel" + "opencsg.com/csghub-server/notification/notifychannel/channel/sms/client" + "opencsg.com/csghub-server/notification/utils" +) + +type SMSChannel struct { + smsService client.SMSService +} + +func NewSMSChannel(smsService client.SMSService) notifychannel.Notifier { + return &SMSChannel{ + smsService: smsService, + } +} + +var _ notifychannel.Notifier = (*SMSChannel)(nil) + +func (s *SMSChannel) IsFormatRequired() bool { + return false +} + +func (s *SMSChannel) Send(ctx context.Context, req *notifychannel.NotifyRequest) error { + if err := req.Receiver.Validate(); err != nil { + return fmt.Errorf("invalid receiver: %w", err) + } + + var smsReq types.SMSReq + if req.Message != nil { + if extractedSMSReq, ok := req.Message.(types.SMSReq); ok { + smsReq = extractedSMSReq + } else { + slog.Error("invalid sms message format", "message type", fmt.Sprintf("%T", req.Message)) + return fmt.Errorf("invalid sms message format") + } + } + + if err := s.smsService.Send(smsReq); err != nil { + return utils.NewErrSendMsg(err, "failed to send sms") // should not print the message, it contains sensitive information + } + + return nil +} diff --git a/notification/notifychannel/channel/sms/sms_test.go b/notification/notifychannel/channel/sms/sms_test.go new file mode 100644 index 000000000..f75429802 --- /dev/null +++ b/notification/notifychannel/channel/sms/sms_test.go @@ -0,0 +1,238 @@ +package sms + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "opencsg.com/csghub-server/common/types" + "opencsg.com/csghub-server/notification/notifychannel" +) + +// MockSMSService is a mock implementation of client.SMSService +type MockSMSService struct { + mock.Mock +} + +func (m *MockSMSService) Send(req types.SMSReq) error { + args := m.Called(req) + return args.Error(0) +} + +func TestSMSChannel_Send(t *testing.T) { + tests := []struct { + name string + request *notifychannel.NotifyRequest + mockSetup func(*MockSMSService) + expectedError string + expectedCalled bool + }{ + { + name: "successful send with valid SMS request", + request: ¬ifychannel.NotifyRequest{ + Message: types.SMSReq{ + PhoneNumbers: []string{"+1234567890"}, + SignName: "TestSign", + TemplateCode: "SMS_123456", + Params: []string{"code=123456"}, + MapParams: map[string]string{ + "code": "123456", + }, + }, + Receiver: ¬ifychannel.Receiver{ + IsBroadcast: false, + Recipients: map[string][]string{ + "user_phone_numbers": {"+1234567890"}, + }, + }, + }, + mockSetup: func(m *MockSMSService) { + m.On("Send", mock.AnythingOfType("types.SMSReq")).Return(nil) + }, + expectedError: "", + expectedCalled: true, + }, + { + name: "successful send with broadcast receiver", + request: ¬ifychannel.NotifyRequest{ + Message: types.SMSReq{ + PhoneNumbers: []string{"+1234567890", "+0987654321"}, + SignName: "TestSign", + TemplateCode: "_123456", + Params: []string{"code=123456"}, + }, + Receiver: ¬ifychannel.Receiver{ + IsBroadcast: true, + Recipients: map[string][]string{}, + }, + }, + mockSetup: func(m *MockSMSService) { + m.On("Send", mock.AnythingOfType("types.SMSReq")).Return(nil) + }, + expectedError: "", + expectedCalled: true, + }, + { + name: "error when receiver validation fails - nil receiver", + request: ¬ifychannel.NotifyRequest{ + Message: types.SMSReq{ + PhoneNumbers: []string{"+1234567890"}, + SignName: "TestSign", + TemplateCode: "SMS_123456", + Params: []string{"code=123456"}, + }, + Receiver: nil, + }, + mockSetup: func(m *MockSMSService) { + // No expectations set - should not be called + }, + expectedError: "invalid receiver: receiver cannot be nil", + expectedCalled: false, + }, + { + name: "error when receiver validation fails - no recipients", + request: ¬ifychannel.NotifyRequest{ + Message: types.SMSReq{ + PhoneNumbers: []string{"+1234567890"}, + SignName: "TestSign", + TemplateCode: "SMS_123456", + Params: []string{"code=123456"}, + }, + Receiver: ¬ifychannel.Receiver{ + IsBroadcast: false, + Recipients: map[string][]string{}, + }, + }, + mockSetup: func(m *MockSMSService) { + // No expectations set - should not be called + }, + expectedError: "invalid receiver: at least one recipient type must be specified", + expectedCalled: false, + }, + { + name: "error when receiver validation fails - empty recipients", + request: ¬ifychannel.NotifyRequest{ + Message: types.SMSReq{ + PhoneNumbers: []string{"+1234567890"}, + SignName: "TestSign", + TemplateCode: "SMS_123456", + Params: []string{"code=123456"}, + }, + Receiver: ¬ifychannel.Receiver{ + IsBroadcast: false, + Recipients: map[string][]string{ + "user_phone_numbers": {}, + }, + }, + }, + mockSetup: func(m *MockSMSService) { + // No expectations set - should not be called + }, + expectedError: "invalid receiver: at least one recipient must be specified", + expectedCalled: false, + }, + { + name: "error when message is not SMSReq type", + request: ¬ifychannel.NotifyRequest{ + Message: "invalid message type", + Receiver: ¬ifychannel.Receiver{ + IsBroadcast: false, + Recipients: map[string][]string{ + "user_phone_numbers": {"+1234567890"}, + }, + }, + }, + mockSetup: func(m *MockSMSService) { + // No expectations set - should not be called + }, + expectedError: "invalid sms message format", + expectedCalled: false, + }, + { + name: "error when message is nil", + request: ¬ifychannel.NotifyRequest{ + Message: nil, + Receiver: ¬ifychannel.Receiver{ + IsBroadcast: false, + Recipients: map[string][]string{ + "user_phone_numbers": {"+1234567890"}, + }, + }, + }, + mockSetup: func(m *MockSMSService) { + // Should be called with empty SMSReq + m.On("Send", types.SMSReq{}).Return(nil) + }, + expectedError: "", + expectedCalled: true, + }, + { + name: "error when SMS service fails", + request: ¬ifychannel.NotifyRequest{ + Message: types.SMSReq{ + PhoneNumbers: []string{"+1234567890"}, + SignName: "TestSign", + TemplateCode: "SMS_123456", + Params: []string{"code=123456"}, + }, + Receiver: ¬ifychannel.Receiver{ + IsBroadcast: false, + Recipients: map[string][]string{ + "user_phone_numbers": {"+1234567890"}, + }, + }, + }, + mockSetup: func(m *MockSMSService) { + m.On("Send", mock.AnythingOfType("types.SMSReq")).Return(errors.New("SMS service error")) + }, + expectedError: "failed to send sms", + expectedCalled: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup + mockSMS := &MockSMSService{} + tt.mockSetup(mockSMS) + + channel := NewSMSChannel(mockSMS) + ctx := context.Background() + + // Execute + err := channel.Send(ctx, tt.request) + + // Assert + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + } + + if tt.expectedCalled { + mockSMS.AssertExpectations(t) + } else { + mockSMS.AssertNotCalled(t, "Send") + } + }) + } +} + +func TestSMSChannel_IsFormatRequired(t *testing.T) { + mockSMS := &MockSMSService{} + channel := NewSMSChannel(mockSMS) + + assert.False(t, channel.IsFormatRequired()) +} + +func TestNewSMSChannel(t *testing.T) { + mockSMS := &MockSMSService{} + channel := NewSMSChannel(mockSMS) + + assert.NotNil(t, channel) + assert.Implements(t, (*notifychannel.Notifier)(nil), channel) +} diff --git a/notification/notifychannel/factory/register.go b/notification/notifychannel/factory/register.go index 2ef0aa93d..4919c615b 100644 --- a/notification/notifychannel/factory/register.go +++ b/notification/notifychannel/factory/register.go @@ -5,25 +5,26 @@ import ( "opencsg.com/csghub-server/builder/store/database" "opencsg.com/csghub-server/common/config" + "opencsg.com/csghub-server/common/types" email "opencsg.com/csghub-server/notification/notifychannel/channel/email" emailclient "opencsg.com/csghub-server/notification/notifychannel/channel/email/client" internalmsg "opencsg.com/csghub-server/notification/notifychannel/channel/internalmsg" -) - -const ( - ChannelNameInternalMessage = "internal-message" - ChannelNameEmail = "email" + "opencsg.com/csghub-server/notification/notifychannel/channel/sms" + smsclientfactory "opencsg.com/csghub-server/notification/notifychannel/channel/sms/client/factory" ) // Register channels func registerChannels(config *config.Config, factory Factory) { // internal message channel internalMessageChannel := internalmsg.NewChannel(config, database.NewNotificationStore()) - factory.RegisterChannel(ChannelNameInternalMessage, internalMessageChannel) + factory.RegisterChannel(types.MessageChannelInternalMessage.String(), internalMessageChannel) // email channel registerEmailChannel(config, factory) + // sms channel + registerSMSChannel(config, factory) + extendChannels(config, factory) } @@ -41,5 +42,16 @@ func registerEmailChannel(config *config.Config, factory Factory) { } emailChannel := email.NewChannel(config, emailService) - factory.RegisterChannel(ChannelNameEmail, emailChannel) + factory.RegisterChannel(types.MessageChannelEmail.String(), emailChannel) +} + +func registerSMSChannel(config *config.Config, factory Factory) { + smsFactory := smsclientfactory.NewDefaultSMSFactory() + smsService, err := smsFactory.CreateSMSClient(config) + if err != nil { + slog.Error("failed to create sms client", "error", err) + return + } + smsChannel := sms.NewSMSChannel(smsService) + factory.RegisterChannel(types.MessageChannelSMS.String(), smsChannel) }