├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── common ├── consts.go ├── structures.go ├── util.go └── util_test.go ├── device ├── alias.go ├── consts.go ├── device.go └── tag.go ├── httplib └── http.go ├── jpush.go ├── jpush_test.go ├── push ├── audience.go ├── audience_test.go ├── consts.go ├── message.go ├── notification.go ├── options.go ├── options_test.go ├── platform.go ├── platform_test.go └── push.go └── report └── report.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.3 5 | - 1.4 6 | - 1.5 7 | - tip 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yangliang Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JPush API Go Client 2 | ==================== 3 | 4 | [![GoDoc](https://godoc.org/github.com/DeanThompson/jpush-api-go-client?status.svg)](https://godoc.org/github.com/DeanThompson/jpush-api-go-client) [![Build Status](https://travis-ci.org/DeanThompson/jpush-api-go-client.svg?branch=master)](https://travis-ci.org/DeanThompson/jpush-api-go-client) 5 | 6 | # 概述 7 | 8 | 这是 JPush REST API 的 Golang 版本封装开发包,**非官方实现**,只支持 v3 版本。 9 | 10 | 官方 REST API 文档: [http://docs.jpush.cn/display/dev/REST+API](http://docs.jpush.cn/display/dev/REST+API) 11 | 12 | # 安装 13 | 14 | 使用 go get 安装,无任何第三方依赖: 15 | 16 | ```sh 17 | go get github.com/DeanThompson/jpush-api-go-client 18 | ``` 19 | 20 | # 使用方法 21 | 22 | ## 1. 创建 JPushClient 23 | 24 | ```go 25 | import "github.com/DeanThompson/jpush-api-go-client" 26 | 27 | const ( 28 | appKey = "" 29 | masterSecret = "" 30 | ) 31 | 32 | jclient := jpush.NewJPushClient(appKey, masterSecret) 33 | ``` 34 | 35 | ## 2. 逐步构建消息体 36 | 37 | 与推送有关的数据结构都在 push 包里 38 | 39 | ```go 40 | import "github.com/DeanThompson/jpush-api-go-client/push" 41 | ``` 42 | 43 | ### 2.1 创建 Platform 对象 44 | 45 | ```go 46 | platform := push.NewPlatform() 47 | 48 | // 用 Add() 方法添加具体平台参数,可选: "all", "ios", "android" 49 | platform.Add("ios", "android") 50 | 51 | // 或者用 All() 方法设置所有平台 52 | // platform.All() 53 | ``` 54 | 55 | ### 2.2 创建 Audience 对象 56 | 57 | ```go 58 | audience := push.NewAudience() 59 | audience.SetTag([]string{"广州", "深圳"}) // 设置 tag 60 | // audience.SetTagAnd([]string{"北京", "女"}) // 设置 tag_and 61 | // audience.SetAlias([]string{"alias1", "alias2"}) // 设置 alias 62 | // audience.SetRegistrationId([]string{"id1", "id2"}) // 设置 registration_id 63 | // audience.All() 和 platform 一样,可以调用 All() 方法设置所有受众 64 | ``` 65 | 66 | ### 2.3 创建 Notification 对象 67 | 68 | #### 2.3.1 创建 AndroidNotification 对象 69 | 70 | ```go 71 | // android 平台专有的 notification,用 alert 属性初始化 72 | androidNotification := push.NewAndroidNotification("Android Notification Alert") 73 | androidNotification.Title = "title" 74 | // androidNotification.BuilderId = 10086 75 | androidNotification.AddExtra("key", "value") 76 | ``` 77 | 78 | #### 2.3.2 创建 IosNotification 对象 79 | 80 | ```go 81 | // iOS 平台专有的 notification,用 alert 属性初始化 82 | iosNotification := push.NewIosNotification("iOS Notification Alert") 83 | // iosNotification.Sound = "/paht/to/sound" 84 | iosNotification.Badge = 1 // 只支持 int 类型的 badge 85 | // iosNotification.ContentAvailable = true 86 | // iosNotification.Category = "category_name" 87 | // iosNotification.AddExtra("key", "value") 88 | 89 | // Validate 方法可以验证 iOS notification 是否合法 90 | // 一般情况下,开发者不需要直接调用此方法,这个方法会在构造 PushObject 时自动调用 91 | // iosNotification.Validate() 92 | ``` 93 | 94 | #### 2.3.3 创建 WinphoneNotification 对象 95 | 96 | ```go 97 | // Windows Phone 平台专有的 notification,用 alert 属性初始化 98 | wpNotification := push.NewWinphoneNotification("Winphone Notification Alert") 99 | // wpNotification.Title = "Winphone Notification Title" 100 | // wpNotification.OpenPage = "some page" 101 | 102 | // 所有平台的专有 notification 都有 AddExtra 方法,用于添加 extra 信息 103 | wpNotification.AddExtra("extra_key", "extra_value") 104 | ``` 105 | 106 | #### 2.3.4 创建 Notification 对象 107 | 108 | AndroidNotification, IosNotification, WinphoneNotification 三个分别是三种平台专有的 notification。 109 | 110 | Notification 是极光推送的“通知”,包含一个 alert 属性,和可选的三个平台属性。 111 | 112 | ```go 113 | // notification 对象,表示 通知,传递 alert 属性初始化 114 | notification := push.NewNotification("Notification Alert") 115 | notification.Android = androidNotification 116 | notification.Ios = iosNotification 117 | notification.Winphone = wpNotification 118 | ``` 119 | 120 | ### 2.4 创建 Message 对象 121 | 122 | Message 是极光推送的“消息”,也叫透传消息 123 | 124 | ```go 125 | // message 对象,表示 透传消息,用 content 属性初始化 126 | message := push.NewMessage("Message Content must not be empty") 127 | // message.Title = "Message Title" 128 | // message.ContentType = "application/json" 129 | 130 | // 可以调用 AddExtra 方法,添加额外信息 131 | // message.AddExtra("key", 123) 132 | ``` 133 | 134 | ### 2.5 创建 Options 对象 135 | 136 | ```go 137 | // option 对象,表示推送可选项 138 | options := push.NewOptions() 139 | // options.SendNo = 12345 140 | // options.OverrideMsgId = 123333333 141 | 142 | // Options 的 Validate 方法会对 time_to_live 属性做范围限制,以满足 JPush 的规范 143 | options.TimeToLive = 10000000 144 | 145 | // iOS 平台,是否推送生产环境,false 表示开发环境;如果不指定,就是生产环境 146 | options.ApnsProduction = true 147 | 148 | // Options 的 Validate 方法会对 big_push_duration 属性做范围限制,以满足 JPush 的规范 149 | options.BigPushDuration = 1500 150 | 151 | // Options 对象有 Validate 方法,但实际上这里并不会返回错误, 152 | // 而是对 time_to_live 和 big_push_duration 两个值做了范围限制 153 | // 开发者不需要手动调用此方法,在构建 PushObject 时会自动调用 154 | // err := options.Validate() 155 | ``` 156 | 157 | ### 2.6 创建 PushObject 对象 158 | 159 | ```go 160 | payload := push.NewPushObject() 161 | payload.Platform = platform 162 | payload.Audience = audience 163 | payload.Notification = notification 164 | payload.Message = message 165 | payload.Options = options 166 | 167 | // 打印查看 json 序列化的结果,也就是 POST 请求的 body 168 | // data, err := json.Marshal(payload) 169 | // if err != nil { 170 | // fmt.Println("json.Marshal PushObject failed:", err) 171 | // } else { 172 | // fmt.Println("payload:", string(data), "\n") 173 | // } 174 | ``` 175 | 176 | ## 3. 推送/推送验证 177 | 178 | ```go 179 | // result, err := jclient.Push(payload) 180 | result, err := jclient.PushValidate(payload) 181 | if err != nil { 182 | fmt.Print("PushValidate failed:", err) 183 | } else { 184 | fmt.Println("PushValidate result:", result) 185 | } 186 | ``` 187 | 188 | ## 4. 更多示例 189 | 190 | 更多例子可以看这里:[jpush_test.go](jpush_test.go) 191 | -------------------------------------------------------------------------------- /common/consts.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "errors" 4 | 5 | const ( 6 | rateLimitQuotaHeader = "X-Rate-Limit-Limit" 7 | rateLimitRemainingHeader = "X-Rate-Limit-Remaining" 8 | rateLimitResetHeader = "X-Rate-Limit-Reset" 9 | 10 | push_host = "https://api.jpush.cn" 11 | device_host = "https://device.jpush.cn" 12 | report_host = "https://report.jpush.cn" 13 | 14 | PUSH_URL = push_host + "/v3/push" 15 | PUSH_VALIDATE_URL = push_host + "/v3/push/validate" 16 | 17 | // GET /v3/devices/{registration_id} 18 | DEVICE_URL = device_host + "/v3/devices/%s" 19 | 20 | QUERY_TAGS_URL = device_host + "/v3/tags/" 21 | // GET /v3/tags/{tag_value}/registration_ids/{registration_id} 22 | CHECK_TAG_USER_EXISTS_URL = device_host + "/v3/tags/%s/registration_ids/%s" 23 | // POST /v3/tags/{tag_value} 24 | UPDATE_TAG_USERS_URL = device_host + "/v3/tags/%s" 25 | // DELETE /v3/tags/{tag_value} 26 | DELETE_TAG_URL = device_host + "/v3/tags/%s" 27 | 28 | // GET /v3/aliases/{alias_value} 29 | QUERY_ALIAS_URL = device_host + "/v3/aliases/%s" 30 | // DELETE /v3/aliases/{alias_value} 31 | DELETE_ALIAS_URL = device_host + "/v3/aliases/%s" 32 | 33 | // GET /v3/received 34 | RECEIVED_REPORT_URL = report_host + "/v3/received" 35 | ) 36 | 37 | var ( 38 | ErrInvalidPlatform = errors.New(": invalid platform") 39 | ErrMessageContentMissing = errors.New(": msg_content is required.") 40 | ErrContentMissing = errors.New(": notification or message is required") 41 | ErrIosNotificationTooLarge = errors.New(": iOS notification too large") 42 | ) 43 | -------------------------------------------------------------------------------- /common/structures.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type ErrorResult struct { 9 | Code int `json:"code"` 10 | Message string `json:"message"` 11 | } 12 | 13 | func (pe *ErrorResult) String() string { 14 | return fmt.Sprintf("{code: %d, message: \"%s\"}", pe.Code, pe.Message) 15 | } 16 | 17 | type RateLimitInfo struct { 18 | RateLimitQuota int 19 | RateLimitRemaining int 20 | RateLimitReset int 21 | } 22 | 23 | //所有的 HTTP API Response Header 里都加了三项频率控制信息: 24 | // 25 | // X-Rate-Limit-Limit: 当前 AppKey 一个时间窗口内可调用次数 26 | // X-Rate-Limit-Remaining:当前时间窗口剩余的可用次数 27 | // X-Rate-Limit-Reset: 距离时间窗口重置剩余的秒数 28 | func NewRateLimitInfo(resp *http.Response) *RateLimitInfo { 29 | info := &RateLimitInfo{} 30 | info.RateLimitQuota, _ = GetIntHeader(resp, rateLimitQuotaHeader) 31 | info.RateLimitRemaining, _ = GetIntHeader(resp, rateLimitRemainingHeader) 32 | info.RateLimitReset, _ = GetIntHeader(resp, rateLimitResetHeader) 33 | return info 34 | } 35 | 36 | func (info *RateLimitInfo) String() string { 37 | return fmt.Sprintf("{limit: %d, remaining: %d, reset: %d}", 38 | info.RateLimitQuota, info.RateLimitRemaining, info.RateLimitReset) 39 | } 40 | 41 | type ResponseBase struct { 42 | // HTTP 状态码 43 | StatusCode int 44 | 45 | // 频率限制相关 46 | RateLimitInfo *RateLimitInfo 47 | 48 | // 错误相关 49 | Error *ErrorResult `json:"error"` 50 | } 51 | 52 | func NewResponseBase(resp *http.Response) ResponseBase { 53 | rb := ResponseBase{ 54 | StatusCode: resp.StatusCode, 55 | RateLimitInfo: NewRateLimitInfo(resp), 56 | } 57 | if !rb.Ok() { 58 | RespToJson(resp, &rb) 59 | } 60 | return rb 61 | } 62 | 63 | // 根据请求返回的 HTTP 状态码判断推送是否成功 64 | // 规范: 65 | // - 200 一定是正确。所有异常都不使用 200 返回码 66 | // - 业务逻辑上的错误,有特别的错误码尽量使用 4xx,否则使用 400。 67 | // - 服务器端内部错误,无特别错误码使用 500。 68 | // - 业务异常时,返回内容使用 JSON 格式定义 error 信息。 69 | // 70 | // 更多细节: http://docs.jpush.io/server/http_status_code/ 71 | func (rb *ResponseBase) Ok() bool { 72 | return rb.StatusCode == http.StatusOK 73 | } 74 | 75 | func (rb *ResponseBase) String() string { 76 | return fmt.Sprintf("StatusCode: %d, rateLimit: %v, error: %v", 77 | rb.StatusCode, rb.RateLimitInfo, rb.Error) 78 | } 79 | -------------------------------------------------------------------------------- /common/util.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "sort" 9 | "strconv" 10 | ) 11 | 12 | func BasicAuth(username, password string) string { 13 | auth := username + ":" + password 14 | return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) 15 | } 16 | 17 | func RespToJson(resp *http.Response, dest interface{}) error { 18 | body, err := ioutil.ReadAll(resp.Body) 19 | defer resp.Body.Close() 20 | if err != nil { 21 | return err 22 | } 23 | println(string(body)) 24 | return json.Unmarshal(body, &dest) 25 | } 26 | 27 | func ResponseOrError(resp *http.Response, err error) (*ResponseBase, error) { 28 | if err != nil { 29 | return nil, err 30 | } 31 | ret := NewResponseBase(resp) 32 | return &ret, nil 33 | } 34 | 35 | func GetIntHeader(resp *http.Response, key string) (int, error) { 36 | v := resp.Header.Get(key) 37 | return strconv.Atoi(v) 38 | } 39 | 40 | func MinInt(a, b int) int { 41 | if a >= b { 42 | return b 43 | } 44 | return a 45 | } 46 | 47 | func UniqString(a []string) []string { 48 | seen := make(map[string]bool, len(a)) 49 | ret := make([]string, 0, len(a)) 50 | for _, v := range a { 51 | if !seen[v] { 52 | ret = append(ret, v) 53 | seen[v] = true 54 | } 55 | } 56 | return ret 57 | } 58 | 59 | func EqualStringSlice(a []string, b []string) bool { 60 | if len(a) != len(b) { 61 | return false 62 | } 63 | 64 | sort.Strings(a) 65 | sort.Strings(b) 66 | 67 | for i, v := range a { 68 | if v != b[i] { 69 | return false 70 | } 71 | } 72 | 73 | return true 74 | } 75 | -------------------------------------------------------------------------------- /common/util_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "testing" 4 | 5 | func Test_UniqString(t *testing.T) { 6 | s := []string{"a", "a", "a", "b"} 7 | uniq := UniqString(s) 8 | if !EqualStringSlice(uniq, []string{"a", "b"}) { 9 | t.Error("UniqString does not work correctly") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /device/alias.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/DeanThompson/jpush-api-go-client/common" 8 | ) 9 | 10 | type GetAliasUsersResult struct { 11 | common.ResponseBase 12 | 13 | RegistrationIds []string `json:"registration_ids"` 14 | } 15 | 16 | func (result *GetAliasUsersResult) FromResponse(resp *http.Response) error { 17 | result.ResponseBase = common.NewResponseBase(resp) 18 | if !result.Ok() { 19 | return nil 20 | } 21 | return common.RespToJson(resp, &result) 22 | } 23 | 24 | func (result *GetAliasUsersResult) String() string { 25 | return fmt.Sprintf(" RegistrationIds: %v, %v", 26 | result.RegistrationIds, result.ResponseBase.String()) 27 | } 28 | -------------------------------------------------------------------------------- /device/consts.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | const ( 4 | actionAdd = "add" 5 | actionRemove = "remove" 6 | maxAddOrRemoveRegistrationIds = 1000 7 | ) 8 | -------------------------------------------------------------------------------- /device/device.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/DeanThompson/jpush-api-go-client/common" 9 | ) 10 | 11 | type QueryDeviceResult struct { 12 | common.ResponseBase 13 | 14 | // 设备的所有属性,包含tags, alias 15 | Tags []string `json:"tags"` 16 | Alias string `json:"alias"` 17 | } 18 | 19 | func (dir *QueryDeviceResult) FromResponse(resp *http.Response) error { 20 | dir.ResponseBase = common.NewResponseBase(resp) 21 | if !dir.Ok() { 22 | return nil 23 | } 24 | return common.RespToJson(resp, &dir) 25 | } 26 | 27 | func (dir *QueryDeviceResult) String() string { 28 | return fmt.Sprintf(" tags: %v, alias: \"%s\", %v", 29 | dir.Tags, dir.Alias, dir.ResponseBase.String()) 30 | } 31 | 32 | ///////////////////////////////////////////////////// 33 | 34 | type tags struct { 35 | Add []string `json:"add,omitempty"` 36 | Remove []string `json:"remove,omitempty"` 37 | Clear bool `json:"-"` 38 | } 39 | 40 | type DeviceUpdate struct { 41 | // 支持 add, remove 或者空字符串。 42 | // 当 tags 参数为空字符串的时候,表示清空所有的 tags; 43 | // add/remove 下是增加或删除指定的 tags 44 | Tags tags 45 | 46 | // 更新设备的别名属性;当别名为空串时,删除指定设备的别名; 47 | Alias string 48 | 49 | // 手机号码 50 | Mobile string 51 | } 52 | 53 | type deviceUpdateWrapper struct { 54 | Tags interface{} `json:"tags"` 55 | Alias string `json:"alias"` 56 | Mobile string `json:"mobile"` 57 | } 58 | 59 | func NewDeviceUpdate() *DeviceUpdate { 60 | return &DeviceUpdate{ 61 | Tags: tags{}, 62 | } 63 | } 64 | 65 | func (du *DeviceUpdate) MarshalJSON() ([]byte, error) { 66 | wrapper := deviceUpdateWrapper{} 67 | if du.Tags.Clear { 68 | wrapper.Tags = "" 69 | } else { 70 | wrapper.Tags = du.Tags 71 | } 72 | wrapper.Alias = du.Alias 73 | wrapper.Mobile = du.Mobile 74 | return json.Marshal(wrapper) 75 | } 76 | 77 | func (du *DeviceUpdate) AddTags(tags ...string) { 78 | du.Tags.Clear = false 79 | tags = common.UniqString(tags) 80 | du.Tags.Add = common.UniqString(append(du.Tags.Add, tags...)) 81 | } 82 | 83 | func (du *DeviceUpdate) RemoveTags(tags ...string) { 84 | du.Tags.Clear = false 85 | tags = common.UniqString(tags) 86 | du.Tags.Remove = common.UniqString(append(du.Tags.Remove, tags...)) 87 | } 88 | 89 | func (du *DeviceUpdate) ClearAllTags() { 90 | du.Tags.Clear = true 91 | } 92 | 93 | func (du *DeviceUpdate) SetAlias(alias string) { 94 | du.Alias = alias 95 | } 96 | 97 | func (du *DeviceUpdate) SetMobile(mobile string) { 98 | du.Mobile = mobile 99 | } 100 | -------------------------------------------------------------------------------- /device/tag.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/DeanThompson/jpush-api-go-client/common" 8 | ) 9 | 10 | // 查询标签列表请求结果 11 | type GetTagsResult struct { 12 | common.ResponseBase 13 | 14 | Tags []string `json:"tags"` 15 | } 16 | 17 | func (tir *GetTagsResult) FromResponse(resp *http.Response) error { 18 | tir.ResponseBase = common.NewResponseBase(resp) 19 | if !tir.Ok() { 20 | return nil 21 | } 22 | return common.RespToJson(resp, &tir) 23 | } 24 | 25 | func (tir *GetTagsResult) String() string { 26 | return fmt.Sprintf(" tags: %v, %v", tir.Tags, tir.ResponseBase.String()) 27 | } 28 | 29 | // 判断设备与标签的绑定请求结果 30 | type CheckTagUserExistsResult struct { 31 | common.ResponseBase 32 | 33 | Result bool `json:"result"` 34 | } 35 | 36 | func (result *CheckTagUserExistsResult) FromResponse(resp *http.Response) error { 37 | result.ResponseBase = common.NewResponseBase(resp) 38 | if !result.Ok() { 39 | return nil 40 | } 41 | return common.RespToJson(resp, &result) 42 | } 43 | 44 | func (result *CheckTagUserExistsResult) String() string { 45 | return fmt.Sprintf(" result: %v, %v", result.Result, result.ResponseBase) 46 | } 47 | 48 | // 更新标签(与设备的绑定的关系)请求参数 49 | type UpdateTagUsersArgs struct { 50 | RegistrationIds map[string][]string `json:"registration_ids"` 51 | } 52 | 53 | func NewUpdateTagUsersArgs() *UpdateTagUsersArgs { 54 | return &UpdateTagUsersArgs{ 55 | RegistrationIds: make(map[string][]string), 56 | } 57 | } 58 | 59 | func (args *UpdateTagUsersArgs) AddRegistrationIds(ids ...string) { 60 | args.updateRegistrationIds(actionAdd, ids...) 61 | } 62 | 63 | func (args *UpdateTagUsersArgs) RemoveRegistrationIds(ids ...string) { 64 | args.updateRegistrationIds(actionRemove, ids...) 65 | } 66 | 67 | func (args *UpdateTagUsersArgs) updateRegistrationIds(action string, ids ...string) { 68 | if action != actionAdd && action != actionRemove { 69 | return 70 | } 71 | ids = common.UniqString(ids) 72 | current := args.RegistrationIds[action] 73 | if current == nil { 74 | current = make([]string, 0, len(ids)) 75 | } 76 | merged := common.UniqString(append(current, ids...)) 77 | if len(merged) > maxAddOrRemoveRegistrationIds { 78 | merged = merged[:maxAddOrRemoveRegistrationIds] 79 | } 80 | args.RegistrationIds[action] = merged 81 | } 82 | -------------------------------------------------------------------------------- /httplib/http.go: -------------------------------------------------------------------------------- 1 | package httplib 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "reflect" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // HTTP methods we support 18 | const ( 19 | GET = "GET" 20 | POST = "POST" 21 | DELETE = "DELETE" 22 | 23 | // PUT = "PUT" 24 | // HEAD = "HEAD" 25 | ) 26 | 27 | type HTTPClient struct { 28 | client *http.Client 29 | transport *http.Transport 30 | debug bool 31 | } 32 | 33 | func NewClient() *HTTPClient { 34 | c := &HTTPClient{ 35 | client: &http.Client{}, 36 | transport: &http.Transport{MaxIdleConnsPerHost: 10}, 37 | } 38 | c.SetTimeout(10*time.Second, 10*time.Second) 39 | c.client.Transport = c.transport 40 | return c 41 | } 42 | 43 | func (c *HTTPClient) SetDebug(debug bool) *HTTPClient { 44 | c.debug = debug 45 | return c 46 | } 47 | 48 | func (c *HTTPClient) SetTimeout(connTimeout, rwTimeout time.Duration) *HTTPClient { 49 | dialer := &net.Dialer{ 50 | Timeout: connTimeout, 51 | KeepAlive: 30 * time.Second, 52 | } 53 | 54 | c.transport.Dial = func(network, addr string) (net.Conn, error) { 55 | conn, err := dialer.Dial(network, addr) 56 | if err != nil { 57 | return nil, err 58 | } 59 | conn.SetDeadline(time.Now().Add(rwTimeout)) 60 | return conn, nil 61 | } 62 | return c 63 | } 64 | 65 | func (c *HTTPClient) do(method string, url string, headers map[string]string, body io.Reader) (*http.Response, error) { 66 | req, err := http.NewRequest(method, url, body) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | if headers != nil { 72 | for k, v := range headers { 73 | req.Header.Set(k, v) 74 | } 75 | } 76 | 77 | return c.client.Do(req) 78 | } 79 | 80 | func (c *HTTPClient) Get(url string, params map[string]interface{}, headers map[string]string) (*http.Response, error) { 81 | url = formatUrl(url, params) 82 | c.log(GET, url, headers) 83 | return c.do(GET, url, headers, nil) 84 | } 85 | 86 | func (c *HTTPClient) Post(url string, bodyType string, body io.Reader, headers map[string]string) (*http.Response, error) { 87 | // DON'T modify headers passed-in 88 | new_headers := make(map[string]string, len(headers)) 89 | for k, v := range headers { 90 | new_headers[k] = v 91 | } 92 | new_headers["Content-Type"] = bodyType 93 | 94 | return c.do(POST, url, new_headers, body) 95 | } 96 | 97 | func (c *HTTPClient) PostForm(url string, data map[string]interface{}, headers map[string]string) (*http.Response, error) { 98 | c.log(POST, url, data, headers) 99 | 100 | body := strings.NewReader(mapToURLValues(data).Encode()) 101 | return c.Post(url, "application/x-www-form-urlencoded", body, headers) 102 | } 103 | 104 | func (c *HTTPClient) PostJson(url string, data interface{}, headers map[string]string) (*http.Response, error) { 105 | payload, err := json.Marshal(data) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | c.log(POST, url, string(payload), headers) 111 | 112 | return c.Post(url, "application/json", bytes.NewReader(payload), headers) 113 | } 114 | 115 | func (c *HTTPClient) Delete(url string, params map[string]interface{}, headers map[string]string) (*http.Response, error) { 116 | url = formatUrl(url, params) 117 | c.log(DELETE, url, headers) 118 | return c.do(DELETE, url, headers, nil) 119 | } 120 | 121 | func (c *HTTPClient) log(v ...interface{}) { 122 | if c.debug { 123 | vs := []interface{}{"[HTTPClient]"} 124 | log.Println(append(vs, v...)...) 125 | } 126 | } 127 | 128 | func valueToString(data interface{}) string { 129 | value := reflect.ValueOf(data) 130 | switch value.Kind() { 131 | case reflect.Bool: 132 | return strconv.FormatBool(value.Bool()) 133 | 134 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 135 | return strconv.FormatInt(value.Int(), 10) 136 | 137 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 138 | return strconv.FormatUint(value.Uint(), 10) 139 | 140 | case reflect.Float32, reflect.Float64: 141 | return strconv.FormatFloat(value.Float(), 'g', -1, 64) 142 | 143 | case reflect.String: 144 | return value.String() 145 | } 146 | 147 | return "" 148 | } 149 | 150 | func mapToURLValues(data map[string]interface{}) url.Values { 151 | values := url.Values{} 152 | for k, v := range data { 153 | values.Set(k, valueToString(v)) 154 | } 155 | return values 156 | } 157 | 158 | func formatUrl(base string, params map[string]interface{}) string { 159 | if params == nil || len(params) == 0 { 160 | return base 161 | } 162 | 163 | if !strings.Contains(base, "?") { 164 | base += "?" 165 | } 166 | 167 | queryString := mapToURLValues(params).Encode() 168 | 169 | if strings.HasSuffix(base, "?") || strings.HasSuffix(base, "&") { 170 | return base + queryString 171 | } 172 | 173 | return base + "&" + queryString 174 | } 175 | -------------------------------------------------------------------------------- /jpush.go: -------------------------------------------------------------------------------- 1 | package jpush 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/DeanThompson/jpush-api-go-client/common" 9 | "github.com/DeanThompson/jpush-api-go-client/device" 10 | "github.com/DeanThompson/jpush-api-go-client/httplib" 11 | "github.com/DeanThompson/jpush-api-go-client/push" 12 | "github.com/DeanThompson/jpush-api-go-client/report" 13 | ) 14 | 15 | // JPush 的 Golang 推送客户端 16 | // 详情: http://docs.jpush.io/server/rest_api_v3_push/ 17 | type JPushClient struct { 18 | appKey string 19 | masterSecret string 20 | headers map[string]string 21 | http *httplib.HTTPClient 22 | } 23 | 24 | func NewJPushClient(appKey string, masterSecret string) *JPushClient { 25 | client := JPushClient{ 26 | appKey: appKey, 27 | masterSecret: masterSecret, 28 | } 29 | headers := make(map[string]string) 30 | headers["User-Agent"] = "jpush-api-go-client" 31 | headers["Connection"] = "keep-alive" 32 | headers["Authorization"] = common.BasicAuth(appKey, masterSecret) 33 | client.headers = headers 34 | 35 | client.http = httplib.NewClient() 36 | 37 | return &client 38 | } 39 | 40 | // 设置调试模式,调试模式下,会输出日志 41 | func (jpc *JPushClient) SetDebug(debug bool) { 42 | jpc.http.SetDebug(debug) 43 | } 44 | 45 | // 推送 API 46 | func (jpc *JPushClient) Push(payload *push.PushObject) (*push.PushResult, error) { 47 | return jpc.doPush(common.PUSH_URL, payload) 48 | } 49 | 50 | // 推送校验 API, 只用于验证推送调用是否能够成功,与推送 API 的区别在于:不向用户发送任何消息。 51 | func (jpc *JPushClient) PushValidate(payload *push.PushObject) (*push.PushResult, error) { 52 | return jpc.doPush(common.PUSH_VALIDATE_URL, payload) 53 | } 54 | 55 | func (jpc *JPushClient) doPush(url string, payload *push.PushObject) (*push.PushResult, error) { 56 | resp, err := jpc.http.PostJson(url, payload, jpc.headers) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | result := &push.PushResult{} 62 | err = result.FromResponse(resp) 63 | return result, err 64 | } 65 | 66 | // 查询设备(设备的别名与标签) 67 | func (jpc *JPushClient) QueryDevice(registrationId string) (*device.QueryDeviceResult, error) { 68 | url := fmt.Sprintf(common.DEVICE_URL, registrationId) 69 | resp, err := jpc.http.Get(url, nil, jpc.headers) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | result := &device.QueryDeviceResult{} 75 | err = result.FromResponse(resp) 76 | return result, err 77 | } 78 | 79 | // 更新设备 (设置的别名与标签) 80 | func (jpc *JPushClient) UpdateDevice(registrationId string, payload *device.DeviceUpdate) (*common.ResponseBase, error) { 81 | url := fmt.Sprintf(common.DEVICE_URL, registrationId) 82 | resp, err := jpc.http.PostJson(url, payload, jpc.headers) 83 | return common.ResponseOrError(resp, err) 84 | } 85 | 86 | // 查询标签列表 87 | func (jpc *JPushClient) GetTags() (*device.GetTagsResult, error) { 88 | resp, err := jpc.http.Get(common.QUERY_TAGS_URL, nil, jpc.headers) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | result := &device.GetTagsResult{} 94 | err = result.FromResponse(resp) 95 | return result, err 96 | } 97 | 98 | // 判断设备与标签的绑定 99 | func (jpc *JPushClient) CheckTagUserExists(tag string, registrationId string) (*device.CheckTagUserExistsResult, error) { 100 | url := fmt.Sprintf(common.CHECK_TAG_USER_EXISTS_URL, tag, registrationId) 101 | resp, err := jpc.http.Get(url, nil, jpc.headers) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | result := &device.CheckTagUserExistsResult{} 107 | err = result.FromResponse(resp) 108 | return result, err 109 | } 110 | 111 | // 更新标签 (与设备的绑定的关系) 112 | func (jpc *JPushClient) UpdateTagUsers(tag string, payload *device.UpdateTagUsersArgs) (*common.ResponseBase, error) { 113 | url := fmt.Sprintf(common.UPDATE_TAG_USERS_URL, tag) 114 | resp, err := jpc.http.PostJson(url, payload, jpc.headers) 115 | return common.ResponseOrError(resp, err) 116 | } 117 | 118 | // 删除标签 (与设备的绑定关系) 119 | func (jpc *JPushClient) DeleteTag(tag string, platforms []string) (*common.ResponseBase, error) { 120 | url := fmt.Sprintf(common.DELETE_TAG_URL, tag) 121 | params := addPlatformsToParams(platforms) 122 | resp, err := jpc.http.Delete(url, params, jpc.headers) 123 | return common.ResponseOrError(resp, err) 124 | } 125 | 126 | // 查询别名 (与设备的绑定关系) 127 | func (jpc *JPushClient) GetAliasUsers(alias string, platforms []string) (*device.GetAliasUsersResult, error) { 128 | url := fmt.Sprintf(common.QUERY_ALIAS_URL, alias) 129 | params := addPlatformsToParams(platforms) 130 | resp, err := jpc.http.Get(url, params, jpc.headers) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | result := &device.GetAliasUsersResult{} 136 | err = result.FromResponse(resp) 137 | return result, err 138 | } 139 | 140 | // 删除别名 (与设备的绑定关系) 141 | func (jpc *JPushClient) DeleteAlias(alias string, platforms []string) (*common.ResponseBase, error) { 142 | url := fmt.Sprintf(common.DELETE_ALIAS_URL, alias) 143 | params := addPlatformsToParams(platforms) 144 | resp, err := jpc.http.Delete(url, params, jpc.headers) 145 | return common.ResponseOrError(resp, err) 146 | } 147 | 148 | // 送达统计 149 | func (jpc *JPushClient) GetReceivedReport(msgIds []uint64) (*report.ReceiveReport, error) { 150 | ids := make([]string, 0, len(msgIds)) 151 | for _, msgId := range msgIds { 152 | ids = append(ids, strconv.FormatUint(msgId, 10)) 153 | } 154 | params := map[string]interface{}{"msg_ids": strings.Join(ids, ",")} 155 | 156 | resp, err := jpc.http.Get(common.RECEIVED_REPORT_URL, params, jpc.headers) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | result := &report.ReceiveReport{} 162 | err = result.FromResponse(resp) 163 | return result, err 164 | } 165 | 166 | //////////////////////////////////////////////////////////////////////////////// 167 | 168 | func addPlatformsToParams(platforms []string) map[string]interface{} { 169 | if platforms == nil { 170 | return nil 171 | } 172 | params := make(map[string]interface{}) 173 | params["platform"] = strings.Join(platforms, ",") 174 | return params 175 | } 176 | -------------------------------------------------------------------------------- /jpush_test.go: -------------------------------------------------------------------------------- 1 | package jpush 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/DeanThompson/jpush-api-go-client/device" 9 | "github.com/DeanThompson/jpush-api-go-client/push" 10 | ) 11 | 12 | const ( 13 | appKey = "8b7127870ccae51a2c2e6da4" 14 | masterSecret = "55df2bc707d65fb39ca01325" 15 | ) 16 | 17 | var client = NewJPushClient(appKey, masterSecret) 18 | 19 | func init() { 20 | client.SetDebug(true) 21 | } 22 | 23 | func showResultOrError(method string, result interface{}, err error) { 24 | if err != nil { 25 | fmt.Printf("%s failed: %v\n\n", method, err) 26 | } else { 27 | fmt.Printf("%s result: %v\n\n", method, result) 28 | } 29 | } 30 | 31 | ///////////////////// Push ///////////////////// 32 | 33 | func test_Push(t *testing.T) { 34 | // platform 对象 35 | platform := push.NewPlatform() 36 | // 用 Add() 方法添加具体平台参数,可选: "all", "ios", "android" 37 | platform.Add("ios", "android") 38 | // 或者用 All() 方法设置所有平台 39 | // platform.All() 40 | 41 | // audience 对象,表示消息受众 42 | audience := push.NewAudience() 43 | audience.SetTag([]string{"广州", "深圳"}) // 设置 tag 44 | audience.SetTagAnd([]string{"北京", "女"}) // 设置 tag_and 45 | // 和 platform 一样,可以调用 All() 方法设置所有受众 46 | // audience.All() 47 | 48 | // notification 对象,表示 通知,传递 alert 属性初始化 49 | notification := push.NewNotification("Notification Alert") 50 | 51 | // android 平台专有的 notification,用 alert 属性初始化 52 | androidNotification := push.NewAndroidNotification("Android Notification Alert") 53 | androidNotification.Title = "title" 54 | androidNotification.AddExtra("key", "value") 55 | 56 | notification.Android = androidNotification 57 | 58 | // iOS 平台专有的 notification,用 alert 属性初始化 59 | iosNotification := push.NewIosNotification("iOS Notification Alert") 60 | iosNotification.Badge = 1 61 | // Validate 方法可以验证 iOS notification 是否合法 62 | // 一般情况下,开发者不需要直接调用此方法,这个方法会在构造 PushObject 时自动调用 63 | // iosNotification.Validate() 64 | 65 | notification.Ios = iosNotification 66 | 67 | // Windows Phone 平台专有的 notification,用 alert 属性初始化 68 | wpNotification := push.NewWinphoneNotification("Winphone Notification Alert") 69 | // 所有平台的专有 notification 都有 AddExtra 方法,用于添加 extra 信息 70 | wpNotification.AddExtra("key", "value") 71 | wpNotification.AddExtra("extra_key", "extra_value") 72 | 73 | notification.Winphone = wpNotification 74 | 75 | // message 对象,表示 透传消息,用 content 属性初始化 76 | message := push.NewMessage("Message Content must not be empty") 77 | message.Title = "Message Title" 78 | 79 | // option 对象,表示推送可选项 80 | options := push.NewOptions() 81 | // iOS 平台,是否推送生产环境,false 表示开发环境;如果不指定,就是生产环境 82 | options.ApnsProduction = true 83 | // Options 的 Validate 方法会对 time_to_live 属性做范围限制,以满足 JPush 的规范 84 | options.TimeToLive = 10000000 85 | // Options 的 Validate 方法会对 big_push_duration 属性做范围限制,以满足 JPush 的规范 86 | options.BigPushDuration = 1500 87 | 88 | payload := push.NewPushObject() 89 | payload.Platform = platform 90 | payload.Audience = audience 91 | payload.Notification = notification 92 | payload.Message = message 93 | payload.Options = options 94 | 95 | data, err := json.Marshal(payload) 96 | if err != nil { 97 | t.Error("json.Marshal PushObject failed:", err) 98 | } 99 | fmt.Println("payload:", string(data), "\n") 100 | 101 | // Push 会推送到客户端 102 | // result, err := client.Push(payload) 103 | // showErrOrResult("Push", result, err) 104 | 105 | // PushValidate 的参数和 Push 完全一致 106 | // 区别在于,PushValidate 只会验证推送调用成功,不会向用户发送任何消息 107 | result, err := client.PushValidate(payload) 108 | showResultOrError("PushValidate", result, err) 109 | } 110 | 111 | ///////////////////// Device ///////////////////// 112 | 113 | func test_QueryDevice(t *testing.T) { 114 | registrationId := "123456" 115 | info, err := client.QueryDevice(registrationId) 116 | showResultOrError("QueryDevice", info, err) 117 | } 118 | 119 | func test_UpdateDevice(t *testing.T) { 120 | update := device.NewDeviceUpdate() 121 | update.AddTags("tag1", "tag2") 122 | update.SetMobile("13800138000") 123 | registrationId := "123456" 124 | result, err := client.UpdateDevice(registrationId, update) 125 | showResultOrError("UpdateDevice", result, err) 126 | } 127 | 128 | ///////////////////// Tags ///////////////////// 129 | 130 | func test_GetTags(t *testing.T) { 131 | result, err := client.GetTags() 132 | showResultOrError("GetTags", result, err) 133 | } 134 | 135 | func test_CheckTagUserExists(t *testing.T) { 136 | tag := "tag1" 137 | registrationId := "090c1f59f89" 138 | result, err := client.CheckTagUserExists(tag, registrationId) 139 | showResultOrError("CheckTagUserExists", result, err) 140 | } 141 | 142 | func test_UpdateTagUsers(t *testing.T) { 143 | args := device.NewUpdateTagUsersArgs() 144 | args.AddRegistrationIds("123", "234", "345") 145 | args.RemoveRegistrationIds("abc", "bcd") 146 | fmt.Println("UpdateTagUsersArgs", args.RegistrationIds) 147 | result, err := client.UpdateTagUsers("tag1", args) 148 | showResultOrError("UpdateTagUsers", result, err) 149 | } 150 | 151 | func test_DeleteTag(t *testing.T) { 152 | result, err := client.DeleteTag("tag1", nil) 153 | showResultOrError("DeleteTag", result, err) 154 | 155 | result, err = client.DeleteTag("tag2", []string{"ios", "android"}) 156 | showResultOrError("DeleteTag", result, err) 157 | } 158 | 159 | ///////////////////// Alias ///////////////////// 160 | 161 | func test_GetAliasUsers(t *testing.T) { 162 | result, err := client.GetAliasUsers("alias1", nil) 163 | showResultOrError("GetAliasUsers", result, err) 164 | 165 | result, err = client.GetAliasUsers("alias1", []string{"ios", "android"}) 166 | showResultOrError("GetAliasUsers", result, err) 167 | } 168 | 169 | func test_DeleteAlias(t *testing.T) { 170 | result, err := client.DeleteAlias("alias1", nil) 171 | showResultOrError("DeleteAlias", result, err) 172 | 173 | result, err = client.DeleteAlias("alias1", []string{"ios", "android"}) 174 | showResultOrError("DeleteAlias", result, err) 175 | } 176 | 177 | ///////////////////// Report ///////////////////// 178 | 179 | func test_GetReceivedReport(t *testing.T) { 180 | msgIds := []uint64{1613113584, 1229760629} 181 | result, err := client.GetReceivedReport(msgIds) 182 | showResultOrError("GetReceivedReport", result, err) 183 | } 184 | 185 | func Test_Starter(t *testing.T) { 186 | test_Push(t) 187 | 188 | test_QueryDevice(t) 189 | test_UpdateDevice(t) 190 | 191 | test_GetTags(t) 192 | test_CheckTagUserExists(t) 193 | test_UpdateTagUsers(t) 194 | test_DeleteTag(t) 195 | 196 | test_GetAliasUsers(t) 197 | test_DeleteAlias(t) 198 | 199 | test_GetReceivedReport(t) 200 | } 201 | -------------------------------------------------------------------------------- /push/audience.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import "github.com/DeanThompson/jpush-api-go-client/common" 4 | 5 | // 推送设备对象,表示一条推送可以被推送到哪些设备列表。 6 | // 确认推送设备对象,JPush 提供了多种方式,比如:别名、标签、注册ID、分群、广播等。 7 | type Audience struct { 8 | isAll bool // 是否推送给所有对象,如果是,value 无效 9 | value map[string][]string 10 | } 11 | 12 | func NewAudience() *Audience { 13 | return &Audience{} 14 | } 15 | 16 | func (a *Audience) Value() interface{} { 17 | if a.isAll { 18 | return ALL 19 | } 20 | return a.value 21 | } 22 | 23 | func (a *Audience) All() { 24 | a.isAll = true 25 | } 26 | 27 | func (a *Audience) SetTag(tags []string) { 28 | a.set("tag", tags) 29 | } 30 | 31 | func (a *Audience) SetTagAnd(tagAnds []string) { 32 | a.set("tag_and", tagAnds) 33 | } 34 | 35 | func (a *Audience) SetAlias(alias []string) { 36 | a.set("alias", alias) 37 | } 38 | 39 | func (a *Audience) SetRegistrationId(ids []string) { 40 | a.set("registration_id", ids) 41 | } 42 | 43 | func (a *Audience) set(key string, v []string) { 44 | a.isAll = false 45 | if a.value == nil { 46 | a.value = make(map[string][]string) 47 | } 48 | 49 | a.value[key] = common.UniqString(v) 50 | } 51 | -------------------------------------------------------------------------------- /push/audience_test.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_Audience(t *testing.T) { 9 | a := NewAudience() 10 | a.SetTag([]string{"深圳", "广州", "北京", "北京", "北京"}) 11 | fmt.Println("SetTag:", a.Value()) 12 | 13 | a.SetTagAnd([]string{"深圳", "女"}) 14 | fmt.Println("SetTagAnd:", a.Value()) 15 | 16 | a.SetAlias([]string{"4314", "892", "4531"}) 17 | fmt.Println("SetAlias:", a.Value()) 18 | 19 | a.SetRegistrationId([]string{"4312kjklfds2", "8914afd2", "45fdsa31"}) 20 | fmt.Println("SetRegistrationId:", a.Value()) 21 | 22 | a.All() 23 | fmt.Println("All:", a.Value()) 24 | } 25 | -------------------------------------------------------------------------------- /push/consts.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | const ( 4 | // all audiences, all platforms 5 | ALL = "all" 6 | 7 | PLATFORM_IOS = "ios" 8 | PLATFORM_ANDROID = "android" 9 | PLATFORM_WP = "winphone" 10 | 11 | IosNotificationMaxSize = 2000 12 | 13 | MaxTimeToLive = 10 * 24 * 60 * 60 // 10 天 14 | MaxBigPushDuration = 1400 15 | ) 16 | -------------------------------------------------------------------------------- /push/message.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | // 应用内消息。或者称作:自定义消息,透传消息。 4 | // 此部分内容不会展示到通知栏上,JPush SDK 收到消息内容后透传给 App。 5 | // 收到消息后 App 需要自行处理。 6 | type Message struct { 7 | Content string `json:"msg_content"` 8 | Title string `json:"title,omitempty"` 9 | ContentType string `json:"content_type,omitempty"` 10 | Extras map[string]interface{} `json:"extras,omitempty"` 11 | } 12 | 13 | func NewMessage(content string) *Message { 14 | return &Message{Content: content} 15 | } 16 | 17 | func (m *Message) Validate() error { 18 | 19 | return nil 20 | } 21 | 22 | func (m *Message) AddExtra(key string, value interface{}) { 23 | if m.Extras == nil { 24 | m.Extras = make(map[string]interface{}) 25 | } 26 | m.Extras[key] = value 27 | } 28 | -------------------------------------------------------------------------------- /push/notification.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/DeanThompson/jpush-api-go-client/common" 7 | ) 8 | 9 | // “通知”对象,是一条推送的实体内容对象之一(另一个是“消息”) 10 | type Notification struct { 11 | Alert string `json:"alert,omitempty"` 12 | Android *AndroidNotification `json:"android,omitempty"` 13 | Ios *IosNotification `json:"ios,omitempty"` 14 | Winphone *WinphoneNotification `json:"winphone,omitempty"` 15 | } 16 | 17 | func NewNotification(alert string) *Notification { 18 | return &Notification{Alert: alert} 19 | } 20 | 21 | func (n *Notification) Validate() error { 22 | if n.Ios != nil { 23 | return n.Ios.Validate() 24 | } 25 | return nil 26 | } 27 | 28 | // 平台通用的通知属性 29 | type platformNotification struct { 30 | Alert string `json:"alert"` // required 31 | Extras map[string]interface{} `json:"extras,omitempty"` 32 | } 33 | 34 | func (nc *platformNotification) AddExtra(key string, value interface{}) { 35 | if nc.Extras == nil { 36 | nc.Extras = make(map[string]interface{}) 37 | } 38 | nc.Extras[key] = value 39 | } 40 | 41 | // Android 平台上的通知。 42 | type AndroidNotification struct { 43 | platformNotification 44 | 45 | Title string `json:"title,omitempty"` 46 | BuilderId int `json:"builder_id,omitempty"` 47 | } 48 | 49 | func NewAndroidNotification(alert string) *AndroidNotification { 50 | n := &AndroidNotification{} 51 | n.Alert = alert 52 | return n 53 | } 54 | 55 | // iOS 平台上 APNs 通知。 56 | type IosNotification struct { 57 | platformNotification 58 | 59 | Sound string `json:"sound,omitempty"` 60 | Badge int `json:"badge,omitempty"` 61 | ContentAvailable bool `json:"content-available,omitempty"` 62 | Category string `json:"category,omitempty"` 63 | } 64 | 65 | func NewIosNotification(alert string) *IosNotification { 66 | a := &IosNotification{} 67 | a.Alert = alert 68 | return a 69 | } 70 | 71 | // APNs 协议定义通知长度为 2048 字节。 72 | // JPush 因为需要重新组包,并且考虑一点安全冗余, 73 | // 要求"iOS":{ } 及大括号内的总体长度不超过:2000 个字节。 74 | func (in *IosNotification) Validate() error { 75 | data, err := json.Marshal(in) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if len("iOS:{}")+len(string(data)) >= IosNotificationMaxSize { 81 | return common.ErrIosNotificationTooLarge 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // Windows Phone 平台上的通知。 88 | type WinphoneNotification struct { 89 | platformNotification 90 | 91 | Title string `json:"title,omitempty"` 92 | OpenPage string `json:"_open_page,omitempty"` 93 | } 94 | 95 | func NewWinphoneNotification(alert string) *WinphoneNotification { 96 | w := &WinphoneNotification{} 97 | w.Alert = alert 98 | return w 99 | } 100 | -------------------------------------------------------------------------------- /push/options.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import "github.com/DeanThompson/jpush-api-go-client/common" 4 | 5 | // 推送可选项。 6 | type Options struct { 7 | SendNo int `json:"sendno,omitempty"` 8 | TimeToLive int `json:"time_to_live,omitempty"` 9 | OverrideMsgId int64 `json:"override_msg_id,omitempty"` 10 | ApnsProduction bool `json:"apns_production"` 11 | BigPushDuration int `json:"big_push_duration,omitempty"` 12 | } 13 | 14 | func NewOptions() *Options { 15 | return &Options{} 16 | } 17 | 18 | // 可选项的校验没有那么严格,目前而言并没有出错的情况 19 | // 只是针对某些值做一下范围限制 20 | func (self *Options) Validate() error { 21 | if self.TimeToLive > 0 { 22 | self.TimeToLive = common.MinInt(self.TimeToLive, MaxTimeToLive) 23 | } 24 | 25 | if self.BigPushDuration > 0 { 26 | self.BigPushDuration = common.MinInt(self.BigPushDuration, MaxBigPushDuration) 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /push/options_test.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func Test_Option(t *testing.T) { 9 | option := NewOptions() 10 | data, _ := json.Marshal(option) 11 | if string(data) != `{"apns_production":false}` { 12 | t.Error("apns_production should be false by default ") 13 | } 14 | 15 | option.ApnsProduction = true 16 | data, _ = json.Marshal(option) 17 | if string(data) != `{"apns_production":true}` { 18 | t.Error("apns_production should be true after setted") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /push/platform.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import "github.com/DeanThompson/jpush-api-go-client/common" 4 | 5 | type Platform struct { 6 | value []string 7 | } 8 | 9 | func NewPlatform() *Platform { 10 | return &Platform{} 11 | } 12 | 13 | // 如果有 "all",只会返回字符串 "all" 14 | // 其他情况都是 []string{},包含具体的平台参数 15 | func (p *Platform) Value() interface{} { 16 | if p.has(ALL) { 17 | return ALL 18 | } 19 | 20 | return p.value 21 | } 22 | 23 | func (p *Platform) All() { 24 | p.value = []string{ALL} 25 | } 26 | 27 | // 添加 platform,可选传参: "all", "ios", "android", "winphone" 28 | func (p *Platform) Add(platforms ...string) error { 29 | if len(platforms) == 0 { 30 | return nil 31 | } 32 | 33 | if p.value == nil { 34 | p.value = make([]string, 0) 35 | } 36 | 37 | // 去重 38 | platforms = common.UniqString(platforms) 39 | 40 | for _, platform := range platforms { 41 | if !isValidPlatform(platform) { 42 | return common.ErrInvalidPlatform 43 | } 44 | 45 | // 不要重复添加,如果有 set 就方便了 46 | if !p.has(platform) { 47 | p.value = append(p.value, platform) 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func (p *Platform) has(platform string) bool { 54 | if p.value == nil { 55 | return false 56 | } 57 | for _, v := range p.value { 58 | if v == ALL || v == platform { 59 | return true 60 | } 61 | } 62 | return false 63 | } 64 | 65 | func isValidPlatform(platform string) bool { 66 | switch platform { 67 | case ALL, PLATFORM_IOS, PLATFORM_ANDROID, PLATFORM_WP: 68 | return true 69 | } 70 | return false 71 | } 72 | -------------------------------------------------------------------------------- /push/platform_test.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/DeanThompson/jpush-api-go-client/common" 7 | ) 8 | 9 | func Test_has(t *testing.T) { 10 | p := NewPlatform() 11 | 12 | // 不用 Add 方法,直接赋值 13 | p.value = []string{"all"} 14 | 15 | if !p.has("ios") { 16 | t.Error("`all` platform should contain `ios`") 17 | } 18 | 19 | p.value = []string{"ios", "android"} 20 | if !p.has("android") { 21 | t.Error("platform should has `android`") 22 | } 23 | if p.has("winphone") { 24 | t.Error("platform should not has `winphone`") 25 | } 26 | } 27 | 28 | func Test_Add(t *testing.T) { 29 | p := NewPlatform() 30 | 31 | err := p.Add("invalid") 32 | if err == nil || err != common.ErrInvalidPlatform { 33 | t.Error("p.Add should return ErrInvalidPlatform") 34 | } 35 | 36 | ps := []string{"ios", "android"} 37 | err = p.Add(ps...) 38 | if err != nil { 39 | t.Error("p.Add should return no error") 40 | } 41 | 42 | if !common.EqualStringSlice(p.value, ps) { 43 | t.Error("platforms not the same as added") 44 | } 45 | } 46 | 47 | func Test_Value(t *testing.T) { 48 | p := NewPlatform() 49 | 50 | p.Add("all", "ios") 51 | v, ok := p.Value().(string) 52 | if !ok { 53 | t.Error("p.Value should return a string type") 54 | } 55 | if v != "all" { 56 | t.Error("p.Value should return `all`") 57 | } 58 | 59 | p.value = nil 60 | 61 | added := []string{"winphone", "android"} 62 | p.Add(added...) 63 | ps, ok := p.Value().([]string) 64 | if !ok { 65 | t.Error("p.Value should return a slice of string") 66 | } 67 | if !common.EqualStringSlice(added, ps) { 68 | t.Errorf("p.Value should return %v", added) 69 | } 70 | } 71 | 72 | func Test_All(t *testing.T) { 73 | p := NewPlatform() 74 | 75 | p.Add("ios", "android") 76 | 77 | p.All() 78 | 79 | if !common.EqualStringSlice([]string{"all"}, p.value) { 80 | t.Error("All() does not work correctly") 81 | } 82 | 83 | v, ok := p.Value().(string) 84 | if !ok { 85 | t.Error("p.Value should return a string type") 86 | } 87 | if v != "all" { 88 | t.Error("p.Value should return `all`") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /push/push.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/DeanThompson/jpush-api-go-client/common" 9 | ) 10 | 11 | type Validator interface { 12 | Validate() error 13 | } 14 | 15 | // 一个推送对象,表示一条推送相关的所有信息。 16 | type PushObject struct { 17 | Platform *Platform `json:"platform"` 18 | Audience *Audience `json:"audience"` 19 | Notification *Notification `json:"notification"` 20 | Message *Message `json:"message"` 21 | Options *Options `json:"options"` 22 | } 23 | 24 | func NewPushObject() *PushObject { 25 | return &PushObject{} 26 | } 27 | 28 | func (po *PushObject) Validate() error { 29 | if po.Notification == nil && po.Message == nil { 30 | return common.ErrContentMissing 31 | } 32 | 33 | if po.Notification != nil { 34 | if err := po.Notification.Validate(); err != nil { 35 | return err 36 | } 37 | } 38 | 39 | if po.Message != nil { 40 | if err := po.Message.Validate(); err != nil { 41 | return err 42 | } 43 | } 44 | 45 | if po.Options != nil { 46 | if err := po.Options.Validate(); err != nil { 47 | return err 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // 实现 Marshaler interface 55 | func (po *PushObject) MarshalJSON() ([]byte, error) { 56 | if err := po.Validate(); err != nil { 57 | return nil, err 58 | } 59 | 60 | wrapper := pushObjectWrapper{ 61 | Platform: po.Platform.Value(), 62 | Audience: po.Audience.Value(), 63 | Notification: po.Notification, 64 | Message: po.Message, 65 | Options: po.Options, 66 | } 67 | return json.Marshal(wrapper) 68 | } 69 | 70 | type pushObjectWrapper struct { 71 | Platform interface{} `json:"platform"` 72 | Audience interface{} `json:"audience"` 73 | Notification *Notification `json:"notification,omitempty"` 74 | Message *Message `json:"message,omitempty"` 75 | Options *Options `json:"options,omitempty"` 76 | } 77 | 78 | type PushResult struct { 79 | common.ResponseBase 80 | 81 | // 成功时 msg_id 是 string 类型。。。 82 | // 失败时 msg_id 是 int 类型。。。 83 | MsgId interface{} `json:"msg_id"` 84 | SendNo string `json:"sendno"` 85 | } 86 | 87 | // 成功: {"sendno":"18", "msg_id":"1828256757"} 88 | // 失败: {"msg_id": 1035959738, "error": {"message": "app_key does not exist", "code": 1008}} 89 | func (pr *PushResult) FromResponse(resp *http.Response) error { 90 | pr.ResponseBase = common.NewResponseBase(resp) 91 | if !pr.Ok() { 92 | return nil 93 | } 94 | return common.RespToJson(resp, &pr) 95 | } 96 | 97 | func (pr *PushResult) String() string { 98 | f := " msg_id: %v, sendno: \"%s\", \"%s\"" 99 | return fmt.Sprintf(f, pr.MsgId, pr.SendNo, pr.ResponseBase.String()) 100 | } 101 | -------------------------------------------------------------------------------- /report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/DeanThompson/jpush-api-go-client/common" 9 | ) 10 | 11 | type ReceivedReportNode struct { 12 | MsgId uint64 `json:"msg_id"` 13 | AndroidReceived int `json:"android_received"` // Android 送达。如果无此项数据则为 null 14 | IosApnsSent int `json:"ios_apns_sent"` // iOS 推送成功。如果无此项数据则为 null 15 | IosMsgReceived int `json:"ios_msg_received"` // iOS 自定义消息送达数。如果无此项数据则为null 16 | WpMpnsSent int `json:"wp_mpns_sent"` // winphone 通知送达。如果无此项数据则为 null 17 | } 18 | 19 | func (node *ReceivedReportNode) String() string { 20 | v, _ := json.Marshal(node) 21 | return string(v) 22 | } 23 | 24 | type ReceiveReport struct { 25 | common.ResponseBase 26 | 27 | Report []ReceivedReportNode 28 | } 29 | 30 | func (report *ReceiveReport) FromResponse(resp *http.Response) error { 31 | report.ResponseBase = common.NewResponseBase(resp) 32 | if !report.Ok() { 33 | return nil 34 | } 35 | return common.RespToJson(resp, &report.Report) 36 | } 37 | 38 | func (report *ReceiveReport) String() string { 39 | s, _ := json.Marshal(report.Report) 40 | return fmt.Sprintf(" report: %v, %v", string(s), 41 | report.ResponseBase.String()) 42 | } 43 | --------------------------------------------------------------------------------