diff --git a/README.md b/README.md index ebe4085..2e8cc78 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ **需要先在友盟官网创建应用获得 App Key 和 App Secret!** -+ `Push()` 单播 -+ `Listcast()` 多播 -+ `Broadcast()`广播(默认每天可推送10次) +- `Unicast()` 单播 +- `Listcast()` 多播 +- `Broadcast()`广播(默认每天可推送10次) ```go import ( @@ -17,8 +17,14 @@ import ( ) func main() { - client := umeng.NewClient(false, "app_key", "app_master_secret", 10 * time.Second) - payload := map[string]interface{}{ + cfg := &umeng.Config{ + AppKey: "app_key", + AppSecret: "app_master_secret", + ProductionMode: false, + } + client := umeng.NewClient(cfg) + + demo := umeng.Payload{ "display_type": "notification", "body": map[string]interface{}{ "ticker": "test_ticker", @@ -35,16 +41,31 @@ func main() { }, } - if err := client.Push(payload, "device_token"); err != nil { - log.Fatalf("err: %v", err) + retMsg, err := client.Unicast(context.Background() ,demo, "device_token") + if err != nil { + log.Fatalf("failed to unicast: %v", err) + } + if err := rm.Error(); err != nil { + // business error + log.Fatal(err) } - if err := client.Listcast(payload, "device_token1", "device_token2", "device_token3"); err != nil { - log.Fatalf("err: %v", err) + retMsg, err := client.Listcast(context.Background(), demo, "device_token1", "device_token2", "device_token3") + if err != nil { + log.Fatalf("failed to listcast: %v", err) + } + if err := rm.Error(); err != nil { + // business error + log.Fatal(err) } - if err := client.Broadcast(payload); err != nil { - fmt.Fatalf("err: %v", err) + retMsg, err := client.Broadcast(context.Background(), demo) + if err != nil { + fmt.Fatalf("failed to broadcast: %v", err) + } + if err := rm.Error(); err != nil { + // business error + log.Fatal(err) } } ``` diff --git a/api.go b/api.go index 43a7a0d..112afc4 100644 --- a/api.go +++ b/api.go @@ -1,38 +1,45 @@ package gomeng -import ( - "encoding/json" - "fmt" +import "fmt" + +const ( + baseURL = "https://msgapi.umeng.com" + + apiPush = "api/send" + apiBroadcast = "api/send" ) +type requestType string + const ( - BaseURL = "https://msgapi.umeng.com/api/" + unicastRequest requestType = "unicast" + listcastRequest requestType = "listcast" + broadcastRequest requestType = "broadcast" +) + +type ReturnState string - APIPush = "send" - APIBroadcast = "send" +const ( + SuccessState ReturnState = "SUCCESS" + FailState ReturnState = "FAIL" ) -type ResponseMessage struct { - Ret string `json:"ret"` +type ( Data struct { - MsgID string `json:"msg_id"` - TaskID string `json:"task_id"` - ErrMsg string `json:"error_msg"` - ErrCode string `json:"error_code"` - } `json:"data"` -} - -func (rm *ResponseMessage) Unmarshal(data []byte) error { - if err := json.Unmarshal(data, rm); err != nil { - return err + MessageID string `json:"msg_id"` + TaskID string `json:"task_id"` + ErrMessage string `json:"error_msg"` + ErrCode string `json:"error_code"` } - return nil -} + ResponseMessage struct { + Ret ReturnState `json:"ret"` + Data `json:"data"` + } +) func (rm *ResponseMessage) Error() error { - if rm.Ret != "SUCCESS" { - return fmt.Errorf("Umeng push failed, error message: %s, error code: %s", - rm.Data.ErrMsg, rm.Data.ErrCode) + if rm.Ret == SuccessState { + return nil } - return nil + return fmt.Errorf("error %s: %s", rm.ErrCode, rm.ErrMessage) } diff --git a/docs/umeng_api.md b/docs/umeng_api.md index ce4d9dc..324c6a8 100644 --- a/docs/umeng_api.md +++ b/docs/umeng_api.md @@ -2,170 +2,170 @@ **使用 API 前需要在Web后台的 App 应用信息页面获取 App key 和 App Master Secret,同时在 Web 后台添加服务器 IP 地址至白名单安或关闭IP白名单。** -### 向指定的用户推送消息(单播) +## 向指定的用户推送消息(单播) | 参数 | 说明 | | --- | --- | | deviceToken | 友盟消息推送服务对设备的唯一标识 | | payload | 消息具体内容 | -#### Android 平台 payload +- Android 平台 payload -```json -{ - "appkey": "xx", // 必填,应用唯一标识 - "timestamp": "xx", // 必填,时间戳,10位或者13位均可,时间戳有效期为10分钟 - "type": "xx", // 必填,消息发送类型,其值可以为: - // unicast-单播 - // listcast-列播,要求不超过500个device_token - // filecast-文件播,多个device_token可通过文件形式批量发送 - // broadcast-广播 - // groupcast-组播,按照filter筛选用户群, 请参照filter参数 - // customizedcast,通过alias进行推送,包括以下两种case: - // - alias: 对单个或者多个alias进行推送 - // - file_id: 将alias存放到文件后,根据file_id来推送 - "device_tokens": "xx", // 当type=unicast时必填, 表示指定的单个设备 - // 当type=listcast时必填, 要求不超过500个, 以英文逗号分隔 - "alias_type": "xx", // 当type=customizedcast时必填 - // alias的类型, alias_type可由开发者自定义, 开发者在SDK中 - // 调用setAlias(alias, alias_type)时所设置的alias_type - "alias": "xx", // 当type=customizedcast时, 选填(此参数和file_id二选一) - // 开发者填写自己的alias, 要求不超过500个alias, 多个alias以英文逗号间隔 - // 在SDK中调用setAlias(alias, alias_type)时所设置的alias - "file_id": "xx", // 当type=filecast时,必填,file内容为多条device_token,以回车符分割 - // 当type=customizedcast时,选填(此参数和alias二选一) - // file内容为多条alias,以回车符分隔。注意同一个文件内的alias所对应的alias_type必须和接口参数alias_type一致 - // 使用文件播需要先调用文件上传接口获取file_id,参照"文件上传" - "filter": {}, // 当type=groupcast时必填,用户筛选条件,如用户标签、渠道等 - "payload": { - // 必填,JSON格式,具体消息内容(Android最大为1840B) - "display_type": "xx", // 必填,消息类型: notification(通知)、message(消息) - "body": { - // 必填,消息体 - // 当display_type=message时,body的内容只需填写custom字段 - // 当display_type=notification时,body包含如下参数: - // 通知展现内容: - "ticker": "xx", // 必填,通知栏提示文字 - "title": "xx", // 必填,通知标题 - "text": "xx", // 必填,通知文字描述 + ```json + { + "appkey": "xx", // 必填,应用唯一标识 + "timestamp": "xx", // 必填,时间戳,10位或者13位均可,时间戳有效期为10分钟 + "type": "xx", // 必填,消息发送类型,其值可以为: + // unicast-单播 + // listcast-列播,要求不超过500个device_token + // filecast-文件播,多个device_token可通过文件形式批量发送 + // broadcast-广播 + // groupcast-组播,按照filter筛选用户群, 请参照filter参数 + // customizedcast,通过alias进行推送,包括以下两种case: + // - alias: 对单个或者多个alias进行推送 + // - file_id: 将alias存放到文件后,根据file_id来推送 + "device_tokens": "xx", // 当type=unicast时必填, 表示指定的单个设备 + // 当type=listcast时必填, 要求不超过500个, 以英文逗号分隔 + "alias_type": "xx", // 当type=customizedcast时必填 + // alias的类型, alias_type可由开发者自定义, 开发者在SDK中 + // 调用setAlias(alias, alias_type)时所设置的alias_type + "alias": "xx", // 当type=customizedcast时, 选填(此参数和file_id二选一) + // 开发者填写自己的alias, 要求不超过500个alias, 多个alias以英文逗号间隔 + // 在SDK中调用setAlias(alias, alias_type)时所设置的alias + "file_id": "xx", // 当type=filecast时,必填,file内容为多条device_token,以回车符分割 + // 当type=customizedcast时,选填(此参数和alias二选一) + // file内容为多条alias,以回车符分隔。注意同一个文件内的alias所对应的alias_type必须和接口参数alias_type一致 + // 使用文件播需要先调用文件上传接口获取file_id,参照"文件上传" + "filter": {}, // 当type=groupcast时必填,用户筛选条件,如用户标签、渠道等 + "payload": { + // 必填,JSON格式,具体消息内容(Android最大为1840B) + "display_type": "xx", // 必填,消息类型: notification(通知)、message(消息) + "body": { + // 必填,消息体 + // 当display_type=message时,body的内容只需填写custom字段 + // 当display_type=notification时,body包含如下参数: + // 通知展现内容: + "ticker": "xx", // 必填,通知栏提示文字 + "title": "xx", // 必填,通知标题 + "text": "xx", // 必填,通知文字描述 - // 自定义通知图标: - "icon": "xx", // 可选,状态栏图标ID - "largeIcon": "xx", // 可选,通知栏拉开后左侧图标ID - "img": "xx", // 可选,通知栏大图标的URL链接 + // 自定义通知图标: + "icon": "xx", // 可选,状态栏图标ID + "largeIcon": "xx", // 可选,通知栏拉开后左侧图标ID + "img": "xx", // 可选,通知栏大图标的URL链接 - // 自定义通知声音: - "sound": "xx", // 可选,通知声音 + // 自定义通知声音: + "sound": "xx", // 可选,通知声音 - // 自定义通知样式: - "builder_id": "xx", // 可选,默认为0,用于标识该通知采用的样式。使用该参数时,开发者必须在SDK里面实现自定义通知栏样式 + // 自定义通知样式: + "builder_id": "xx", // 可选,默认为0,用于标识该通知采用的样式。使用该参数时,开发者必须在SDK里面实现自定义通知栏样式 - // 通知到达设备后的提醒方式 - "play_vibrate": "true/false", // 可选,收到通知是否震动,默认为"true" - "play_lights": "true/false", // 可选,收到通知是否闪灯,默认为"true" - "play_sound": "true/false", // 可选,收到通知是否发出声音,默认为"true" + // 通知到达设备后的提醒方式 + "play_vibrate": "true/false", // 可选,收到通知是否震动,默认为"true" + "play_lights": "true/false", // 可选,收到通知是否闪灯,默认为"true" + "play_sound": "true/false", // 可选,收到通知是否发出声音,默认为"true" - // 点击"通知"的后续行为,默认为打开app。 - "after_open": "xx", // 可选,默认为"go_app",值可以为: - // "go_app": 打开应用 - // "go_url": 跳转到URL - // "go_activity": 打开特定的activity - // "go_custom": 用户自定义内容 - "url": "xx", // 当after_open=go_url时必填 - // 通知栏点击后跳转的URL,要求以http或者https开头 - "activity": "xx", // 当after_open=go_activity时必填,通知栏点击后打开的Activity - "custom": {}, // 当display_type=message时必填 - // 当display_type=notification且after_open=go_custom时必填 - // 用户自定义内容,可以为字符串或者JSON格式 + // 点击"通知"的后续行为,默认为打开app。 + "after_open": "xx", // 可选,默认为"go_app",值可以为: + // "go_app": 打开应用 + // "go_url": 跳转到URL + // "go_activity": 打开特定的activity + // "go_custom": 用户自定义内容 + "url": "xx", // 当after_open=go_url时必填 + // 通知栏点击后跳转的URL,要求以http或者https开头 + "activity": "xx", // 当after_open=go_activity时必填,通知栏点击后打开的Activity + "custom": {}, // 当display_type=message时必填 + // 当display_type=notification且after_open=go_custom时必填 + // 用户自定义内容,可以为字符串或者JSON格式 + }, + "extra": { + // 可选,JSON格式,用户自定义key-value。只对"通知"(display_type=notification)生效 + // 可以配合通知到达后,打开App/URL/Activity使用 + "key1": "value1", + "key2": "value2", + } }, - "extra": { - // 可选,JSON格式,用户自定义key-value。只对"通知"(display_type=notification)生效 - // 可以配合通知到达后,打开App/URL/Activity使用 - "key1": "value1", - "key2": "value2", - } - }, - "policy": { - // 可选,发送策略 - "start_time": "xx", // 可选,定时发送时,若不填写表示立即发送 - // 定时发送时间不能小于当前时间 - // 格式: "yyyy-MM-dd HH:mm:ss" - "expire_time": "xx", // 可选,消息过期时间,其值不可小于发送时间或者start_time - "max_send_num": "xx", // 可选,发送限速,每秒发送的最大条数。最小值1000 - "out_biz_no": "xx" // 可选,开发者对消息的唯一标识,服务器会根据这个标识避免重复发送 - }, - "production_mode": "true/false", // 可选,正式/测试模式。默认为true - // 测试模式只会将消息发给测试设备。测试设备需要到web上添加 - // Android: 测试设备属于正式设备的一个子集 - "description": "xx", // 可选,发送消息描述,建议填写 - "mipush": "true/false", // 可选,默认为false。当为true时,表示MIUI、EMUI、Flyme系统设备离线转为系统下发 - "mi_activity": "xx", // 可选,mipush值为true时生效,表示走系统通道时打开指定页面acitivity的完整包路径 - -``` + "policy": { + // 可选,发送策略 + "start_time": "xx", // 可选,定时发送时,若不填写表示立即发送 + // 定时发送时间不能小于当前时间 + // 格式: "yyyy-MM-dd HH:mm:ss" + "expire_time": "xx", // 可选,消息过期时间,其值不可小于发送时间或者start_time + "max_send_num": "xx", // 可选,发送限速,每秒发送的最大条数。最小值1000 + "out_biz_no": "xx" // 可选,开发者对消息的唯一标识,服务器会根据这个标识避免重复发送 + }, + "production_mode": "true/false", // 可选,正式/测试模式。默认为true + // 测试模式只会将消息发给测试设备。测试设备需要到web上添加 + // Android: 测试设备属于正式设备的一个子集 + "description": "xx", // 可选,发送消息描述,建议填写 + "mipush": "true/false", // 可选,默认为false。当为true时,表示MIUI、EMUI、Flyme系统设备离线转为系统下发 + "mi_activity": "xx", // 可选,mipush值为true时生效,表示走系统通道时打开指定页面acitivity的完整包路径 + } + ``` -#### iOS 平台 payload +- iOS 平台 payload -```json -{ - "appkey": "xx", // 必填,应用唯一标识 - "timestamp": "xx", // 必填,时间戳,10位或者13位均可,时间戳有效期为10分钟 - "type": "xx", // 必填,消息发送类型,其值可以为: - // unicast-单播 - // listcast-列播,要求不超过500个device_token - // filecast-文件播,多个device_token可通过文件形式批量发送 - // broadcast-广播 - // groupcast-组播,按照filter筛选用户群, 请参照filter参数 - // customizedcast,通过alias进行推送,包括以下两种case: - // - alias: 对单个或者多个alias进行推送 - // - file_id: 将alias存放到文件后,根据file_id来推送 - "device_tokens": "xx", // 当type=unicast时必填, 表示指定的单个设备 - // 当type=listcast时必填, 要求不超过500个, 以英文逗号分隔 - "alias_type": "xx", // 当type=customizedcast时必填 - // alias的类型, alias_type可由开发者自定义, 开发者在SDK中 - // 调用setAlias(alias, alias_type)时所设置的alias_type - "alias": "xx", // 当type=customizedcast时, 选填(此参数和file_id二选一) - // 开发者填写自己的alias, 要求不超过500个alias, 多个alias以英文逗号间隔 - // 在SDK中调用setAlias(alias, alias_type)时所设置的alias - "file_id": "xx", // 当type=filecast时,必填,file内容为多条device_token,以回车符分割 - // 当type=customizedcast时,选填(此参数和alias二选一) - // file内容为多条alias,以回车符分隔。注意同一个文件内的alias所对应 - // 的alias_type必须和接口参数alias_type一致 - // 使用文件播需要先调用文件上传接口获取file_id,参照"2.4文件上传接口" - "filter": {}, // 当type=groupcast时必填,用户筛选条件,如用户标签、渠道等 - "payload": + ```json { - // 必填,JSON格式,具体消息内容(iOS最大为2012B) - "aps": + "appkey": "xx", // 必填,应用唯一标识 + "timestamp": "xx", // 必填,时间戳,10位或者13位均可,时间戳有效期为10分钟 + "type": "xx", // 必填,消息发送类型,其值可以为: + // unicast-单播 + // listcast-列播,要求不超过500个device_token + // filecast-文件播,多个device_token可通过文件形式批量发送 + // broadcast-广播 + // groupcast-组播,按照filter筛选用户群, 请参照filter参数 + // customizedcast,通过alias进行推送,包括以下两种case: + // - alias: 对单个或者多个alias进行推送 + // - file_id: 将alias存放到文件后,根据file_id来推送 + "device_tokens": "xx", // 当type=unicast时必填, 表示指定的单个设备 + // 当type=listcast时必填, 要求不超过500个, 以英文逗号分隔 + "alias_type": "xx", // 当type=customizedcast时必填 + // alias的类型, alias_type可由开发者自定义, 开发者在SDK中 + // 调用setAlias(alias, alias_type)时所设置的alias_type + "alias": "xx", // 当type=customizedcast时, 选填(此参数和file_id二选一) + // 开发者填写自己的alias, 要求不超过500个alias, 多个alias以英文逗号间隔 + // 在SDK中调用setAlias(alias, alias_type)时所设置的alias + "file_id": "xx", // 当type=filecast时,必填,file内容为多条device_token,以回车符分割 + // 当type=customizedcast时,选填(此参数和alias二选一) + // file内容为多条alias,以回车符分隔。注意同一个文件内的alias所对应 + // 的alias_type必须和接口参数alias_type一致 + // 使用文件播需要先调用文件上传接口获取file_id,参照"2.4文件上传接口" + "filter": {}, // 当type=groupcast时必填,用户筛选条件,如用户标签、渠道等 + "payload": { - // 必填,严格按照APNs定义来填写 - "alert": { - // 当content-available=1时(静默推送),可选; 否则必填 - // 可为JSON类型和字符串类型 - "title": "title", - "subtitle": "subtitle", - "body": "body" + // 必填,JSON格式,具体消息内容(iOS最大为2012B) + "aps": + { + // 必填,严格按照APNs定义来填写 + "alert": { + // 当content-available=1时(静默推送),可选; 否则必填 + // 可为JSON类型和字符串类型 + "title": "title", + "subtitle": "subtitle", + "body": "body" + }, + "badge": "xx", // 可选 + "sound": "xx", // 可选 + "content-available": 1, // 可选,代表静默推送 + "category": "xx", // 可选,注意: iOS8才支持该字段 }, - "badge": "xx", // 可选 - "sound": "xx", // 可选 - "content-available": 1, // 可选,代表静默推送 - "category": "xx", // 可选,注意: iOS8才支持该字段 + "key1": "value1", + "key2": "value2", }, - "key1": "value1", - "key2": "value2", - }, - "policy": - { - // 可选,发送策略 - "start_time": "xx", // 可选,定时发送时间,若不填写表示立即发送。 - // 定时发送时间不能小于当前时间 - // 格式: "yyyy-MM-dd HH:mm:ss" - // 注意,start_time只对任务生效 - "expire_time": "xx", // 可选,消息过期时间,其值不可小于发送时间或者start_time(如果填写了的话) - // 如果不填写此参数,默认为3天后过期。格式同start_time - "out_biz_no": "xx", // 可选,开发者对消息的唯一标识,服务器会根据这个标识避免重复发送 - "apns_collapse_id": "xx" // 可选,多条带有相同apns_collapse_id的消息,iOS设备仅展最新的一条,字段长度不得超过64bytes - }, - "production_mode": "true/false", // 可选,正式/测试模式。默认为true - "description": "xx" // 可选,发送消息描述,建议填写 -} -``` + "policy": + { + // 可选,发送策略 + "start_time": "xx", // 可选,定时发送时间,若不填写表示立即发送。 + // 定时发送时间不能小于当前时间 + // 格式: "yyyy-MM-dd HH:mm:ss" + // 注意,start_time只对任务生效 + "expire_time": "xx", // 可选,消息过期时间,其值不可小于发送时间或者start_time(如果填写了的话) + // 如果不填写此参数,默认为3天后过期。格式同start_time + "out_biz_no": "xx", // 可选,开发者对消息的唯一标识,服务器会根据这个标识避免重复发送 + "apns_collapse_id": "xx" // 可选,多条带有相同apns_collapse_id的消息,iOS设备仅展最新的一条,字段长度不得超过64bytes + }, + "production_mode": "true/false", // 可选,正式/测试模式。默认为true + "description": "xx" // 可选,发送消息描述,建议填写 + } + ``` diff --git a/gomeng.go b/gomeng.go index a5f2446..41dbb6f 100644 --- a/gomeng.go +++ b/gomeng.go @@ -5,22 +5,25 @@ import ( "time" ) -const DefaultTimeout = 10 * time.Second +const defaultTimeout = 10 * time.Second + +type Config struct { + AppKey string `json:"app_key" yaml:"appKey"` + AppSecret string `json:"app_secret" yaml:"appSecret"` + ProductionMode bool `json:"production_mode" yaml:"productionMode"` + Timeout time.Duration `json:"timeout" yaml:"timeout"` +} type Client struct { - productMode bool - key string - secret string - *http.Client + cfg *Config + rawhttp *http.Client } -func NewClient(productMode bool, key, secret string, timeout time.Duration) *Client { +func NewClient(cfg *Config) *Client { return &Client{ - productMode: productMode, - key: key, - secret: secret, - Client: &http.Client{ - Timeout: timeout, + cfg: cfg, + rawhttp: &http.Client{ + Timeout: fallback2DefaultIfZero(cfg.Timeout), }, } } diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..ae9e7b1 --- /dev/null +++ b/helper.go @@ -0,0 +1,33 @@ +package gomeng + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "fmt" + "time" +) + +func sign(method, url, secret string, raw []byte) (string, error) { + buf := bytes.NewBufferString(method) + buf.WriteString(url) + buf.Write(raw) + buf.WriteString(secret) + hasher := md5.New() + if _, err := hasher.Write(buf.Bytes()); err != nil { + return "", err + } + return hex.EncodeToString(hasher.Sum(nil)), nil +} + +func joinSign(url, sign string) string { + // e.g. https://msgapi.umeng.com/api/send?sign=xxx + return fmt.Sprintf("%s?sign=%s", url, sign) +} + +func fallback2DefaultIfZero(timeout time.Duration) time.Duration { + if timeout > 0 { + return timeout + } + return defaultTimeout +} diff --git a/push.go b/push.go index a23a4cb..ba531a6 100644 --- a/push.go +++ b/push.go @@ -1,44 +1,25 @@ package gomeng +import "context" + /** * 推送给单用户(单播) */ -func (c *Client) Push(payload map[string]interface{}, deviceToken string) error { - resp, err := c.doPost(c.genReqParams(payload, "unicast", deviceToken), APIPush) - if err != nil { - return err - } - if err := resp.Error(); err != nil { - return err - } - return nil +func (c *Client) Unicast(ctx context.Context, payload Payload, deviceToken string) (resp *ResponseMessage, err error) { + return c.doPost(ctx, c.genRequestParams(payload, unicastRequest, deviceToken), apiPush) } /** * 推送给多用户(列播) */ -func (c *Client) ListCast(payload map[string]interface{}, deviceTokens ...string) error { - resp, err := c.doPost(c.genReqParams(payload, "listcast", deviceTokens...), APIPush) - if err != nil { - return err - } - if err := resp.Error(); err != nil { - return err - } - return nil +func (c *Client) ListCast(ctx context.Context, payload Payload, deviceTokens ...string) (resp *ResponseMessage, err error) { + return c.doPost(ctx, c.genRequestParams(payload, listcastRequest, deviceTokens...), apiPush) } /** * 推送给所有用户(广播) * 默认每天可推送10次 */ -func (c *Client) Broadcast(payload map[string]interface{}) error { - resp, err := c.doPost(c.genReqParams(payload, "broadcast"), APIBroadcast) - if err != nil { - return err - } - if err := resp.Error(); err != nil { - return err - } - return nil +func (c *Client) Broadcast(ctx context.Context, payload Payload) (resp *ResponseMessage, err error) { + return c.doPost(ctx, c.genRequestParams(payload, broadcastRequest), apiBroadcast) } diff --git a/push_test.go b/push_test.go index fe1928d..3bb830d 100644 --- a/push_test.go +++ b/push_test.go @@ -1,19 +1,18 @@ package gomeng import ( + "context" + "os" "testing" ) -const ( - TestAppKey = "app_key" - TestAppSecret = "app_secret" - TestProductMode = false - TestDeviceToken = "AtOAal-11NoRhG1KJv_aq1aij5O_aWwMlvvklGNu1LmG" -) - -func TestPush2SingleUser(t *testing.T) { - c := NewClient(TestProductMode, TestAppKey, TestAppSecret, DefaultTimeout) - payload := map[string]interface{}{ +func newTest() (client *Client, payload Payload, token string) { + cfg := &Config{ + AppKey: os.Getenv("APP_KEY"), + AppSecret: os.Getenv("APP_SECRET"), + Timeout: defaultTimeout, + } + return NewClient(cfg), Payload{ "display_type": "notification", "body": map[string]interface{}{ "ticker": "test_ticker", @@ -28,57 +27,41 @@ func TestPush2SingleUser(t *testing.T) { "after_open": "go_app", "play_sound": "true", }, - } + }, os.Getenv("DEVICE_TOKEN") +} - if err := c.Push(payload, TestDeviceToken); err != nil { - t.Fatalf("err: %v", err) +func TestPush2SingleUser(t *testing.T) { + c, payload, token := newTest() + rm, err := c.Unicast(context.Background(), payload, token) + if err != nil { + t.Errorf("err: %v", err) + return + } + if err := rm.Error(); err != nil { + t.Errorf("%s failed: %v", t.Name(), err) } } func TestPush2MultiUsers(t *testing.T) { - c := NewClient(TestProductMode, TestAppKey, TestAppSecret, DefaultTimeout) - payload := map[string]interface{}{ - "display_type": "notification", - "body": map[string]interface{}{ - "ticker": "test_ticker", - "title": "test_title", - "text": "test_text", - "builder_id:": 1, - "custom": map[string]interface{}{ - "key1": "value1", - "key2": "value2", - "key3": "value3", - }, - "after_open": "go_app", - "play_sound": "true", - }, + c, payload, token := newTest() + rm, err := c.ListCast(context.Background(), payload, token) + if err != nil { + t.Errorf("err: %v", err) + return } - - if err := c.ListCast(payload, "AtOAal-11NoRhG1KJv_aq1aij5O_aWwMlvvklGNu1LmG"); err != nil { - t.Fatalf("err: %v", err) + if err := rm.Error(); err != nil { + t.Errorf("%s failed: %v", t.Name(), err) } } func TestPush2AllUsers(t *testing.T) { - c := NewClient(TestProductMode, TestAppKey, TestAppSecret, DefaultTimeout) - payload := map[string]interface{}{ - "display_type": "notification", - "body": map[string]interface{}{ - "ticker": "test_ticker", - "title": "test_title", - "text": "test_text", - "builder_id:": 1, - "custom": map[string]interface{}{ - "key1": "value1", - "key2": "value2", - "key3": "value3", - }, - "after_open": "go_app", - "play_sound": "true", - }, + c, payload, _ := newTest() + rm, err := c.Broadcast(context.Background(), payload) + if err != nil { + t.Errorf("err: %v", err) + return } - - if err := c.Broadcast(payload); err != nil { - t.Fatalf("err: %v", err) + if err := rm.Error(); err != nil { + t.Errorf("%s failed: %v", t.Name(), err) } } diff --git a/request.go b/request.go index ca5baae..01477b7 100644 --- a/request.go +++ b/request.go @@ -2,21 +2,25 @@ package gomeng import ( "bytes" + "context" "encoding/json" "fmt" "io" "net/http" + "net/url" "strings" "time" ) -func (c *Client) genReqParams(payload map[string]interface{}, reqType string, deviceTokens ...string) map[string]interface{} { +type Payload map[string]interface{} + +func (c *Client) genRequestParams(payload Payload, reqType requestType, deviceTokens ...string) map[string]interface{} { p := map[string]interface{}{ - "appkey": c.key, + "appkey": c.cfg.AppKey, "timestamp": time.Now().Unix(), "type": reqType, "payload": payload, - "production_mode": c.productMode, + "production_mode": c.cfg.ProductionMode, } if len(deviceTokens) > 0 { p["device_tokens"] = strings.Join(deviceTokens, ",") @@ -24,22 +28,31 @@ func (c *Client) genReqParams(payload map[string]interface{}, reqType string, de return p } -func (c *Client) doPost(param map[string]interface{}, endpoint string) (*ResponseMessage, error) { - url := BaseURL + endpoint - // sign - sign, err := c.doSign(http.MethodPost, url, param) +func (c *Client) doPost(ctx context.Context, param map[string]interface{}, endpoint string) (*ResponseMessage, error) { + // e.g. https://msgapi.umeng.com/api/send + url, err := url.JoinPath(baseURL, endpoint) if err != nil { - return nil, fmt.Errorf("Sign failed, error: %s", err.Error()) + return nil, err } - b, err := json.Marshal(param) if err != nil { return nil, err } - resp, err := c.Post(fmt.Sprintf("%s?sign=%s", url, sign), "application/json", bytes.NewReader(b)) + // do sign + sign, err := sign(http.MethodPost, url, c.cfg.AppSecret, b) if err != nil { - return nil, err + return nil, fmt.Errorf("sign error: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinSign(url, sign), bytes.NewReader(b)) + if err != nil { + return nil, fmt.Errorf("new request error: %w", err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := c.rawhttp.Do(req) + if err != nil { + return nil, fmt.Errorf("request error: %w", err) } defer resp.Body.Close() @@ -49,7 +62,7 @@ func (c *Client) doPost(param map[string]interface{}, endpoint string) (*Respons } rm := &ResponseMessage{} - if err := rm.Unmarshal(body); err != nil { + if err := json.Unmarshal(body, rm); err != nil { return nil, err } return rm, nil diff --git a/sign.go b/sign.go deleted file mode 100644 index f5f0c75..0000000 --- a/sign.go +++ /dev/null @@ -1,25 +0,0 @@ -package gomeng - -import ( - "crypto/md5" - "encoding/hex" - "encoding/json" -) - -/** - * 友盟签名算法 - * 拼接请求方法、url、post-body及应用的app_master_secret - * 将D形成字符串计算MD5值,形成一个32位的十六进制(字母小写)字符串,即为本次请求sign(签名)的值 - */ -func (c *Client) doSign(method string, url string, param map[string]interface{}) (string, error) { - body, err := json.Marshal(param) - if err != nil { - return "", err - } - str := method + url + string(body) + c.secret - hasher := md5.New() - if _, err := hasher.Write([]byte(str)); err != nil { - return "", err - } - return hex.EncodeToString(hasher.Sum(nil)), nil -}