├── LICENSE ├── README.md ├── auth └── auth.go ├── client └── client.go ├── def.go └── req └── req.go /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xinge 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/FrontMage/xinge)](https://goreportcard.com/report/github.com/FrontMage/xinge) 4 | [![GoDoc](https://godoc.org/github.com/FrontMage/xinge?status.svg)](https://godoc.org/github.com/FrontMage/xinge) 5 | 6 | 腾讯信鸽push Golang lib 7 | 8 | `信鸽v3版API的简单封装` 9 | 10 | ## 用法 11 | 12 | ### 安装 13 | `$ go get github.com/FrontMage/xinge` 14 | 15 | ### 安卓单账号push 16 | ```go 17 | import ( 18 | "net/http" 19 | "io/ioutil" 20 | "encoding/json" 21 | "fmt" 22 | "github.com/FrontMage/xinge" 23 | "github.com/FrontMage/xinge/req" 24 | "github.com/FrontMage/xinge/auth" 25 | ) 26 | 27 | func main() { 28 | auther := auth.Auther{AppID: "AppID", SecretKey: "SecretKey"} 29 | pushReq, _ := req.NewSingleAndroidAccountPush("account", "title", "content") 30 | auther.Auth(pushReq) 31 | 32 | c := &http.Client{} 33 | rsp, _ := c.Do(pushReq) 34 | defer rsp.Body.Close() 35 | body, _ := ioutil.ReadAll(rsp.Body) 36 | 37 | r := &xinge.CommonRsp{} 38 | json.Unmarshal(body, r) 39 | fmt.Printf("%+v", r) 40 | } 41 | ``` 42 | 43 | ### 苹果单账号push 44 | ```go 45 | import ( 46 | "net/http" 47 | "io/ioutil" 48 | "encoding/json" 49 | "fmt" 50 | "github.com/FrontMage/xinge/req" 51 | "github.com/FrontMage/xinge/auth" 52 | ) 53 | 54 | func main() { 55 | auther := auth.Auther{AppID: "AppID", SecretKey: "SecretKey"} 56 | pushReq, _ := req.NewSingleIOSAccountPush("account", "title", "content") 57 | auther.Auth(pushReq) 58 | 59 | c := &http.Client{} 60 | rsp, _ := c.Do(pushReq) 61 | defer rsp.Body.Close() 62 | body, _ := ioutil.ReadAll(rsp.Body) 63 | 64 | r := &xinge.CommonRsp{} 65 | json.Unmarshal(body, r) 66 | fmt.Printf("%+v", r) 67 | } 68 | ``` 69 | 70 | ### 安卓多账号push 71 | ```go 72 | auther := auth.Auther{AppID: "AppID", SecretKey: "SecretKey"} 73 | pushReq, _ := req.NewPushReq( 74 | &xinge.Request{}, 75 | req.Platform(xinge.PlatformAndroid), 76 | req.AudienceType(xinge.AdAccountList), 77 | req.MessageType(xinge.MsgTypeNotify), 78 | req.AccountList([]string{"10000031", "10000034"}), 79 | req.PushID("0"), 80 | req.Message(xinge.Message{ 81 | Title: "haha", 82 | Content: "hehe", 83 | }), 84 | ) 85 | auther.Auth(pushReq) 86 | 87 | c := &http.Client{} 88 | rsp, _ := c.Do(pushReq) 89 | defer rsp.Body.Close() 90 | body, _ := ioutil.ReadAll(rsp.Body) 91 | 92 | r := &xinge.CommonRsp{} 93 | json.Unmarshal(body, r) 94 | fmt.Printf("%+v", r) 95 | ``` 96 | 97 | ### iOS多账号push 98 | ```go 99 | auther := auth.Auther{AppID: "AppID", SecretKey: "SecretKey"} 100 | pushReq, _ := req.NewPushReq( 101 | &xinge.Request{}, 102 | req.Platform(xinge.PlatformiOS), 103 | req.EnvDev(), 104 | req.AudienceType(xinge.AdAccountList), 105 | req.MessageType(xinge.MsgTypeNotify), 106 | req.AccountList([]string{"10000031", "10000034"}), 107 | req.PushID("0"), 108 | req.Message(xinge.Message{ 109 | Title: "haha", 110 | Content: "hehe", 111 | }), 112 | ) 113 | auther.Auth(pushReq) 114 | 115 | c := &http.Client{} 116 | rsp, _ := c.Do(pushReq) 117 | defer rsp.Body.Close() 118 | body, _ := ioutil.ReadAll(rsp.Body) 119 | 120 | r := &xinge.CommonRsp{} 121 | json.Unmarshal(body, r) 122 | fmt.Printf("%+v", r) 123 | ``` 124 | 125 | ### 单设备push 126 | ```go 127 | auther := auth.Auther{AppID: "AppID", SecretKey: "SecretKey"} 128 | pushReq, _ := req.NewPushReq( 129 | &xinge.Request{}, 130 | req.Platform(xinge.PlatformiOS), 131 | req.EnvDev(), 132 | req.AudienceType(xinge.AdToken), 133 | req.MessageType(xinge.MsgTypeNotify), 134 | req.TokenList([]string{"10000031", "10000034"}), 135 | req.PushID("0"), 136 | req.Message(xinge.Message{ 137 | Title: "haha", 138 | Content: "hehe", 139 | }), 140 | ) 141 | auther.Auth(pushReq) 142 | 143 | c := &http.Client{} 144 | rsp, _ := c.Do(pushReq) 145 | defer rsp.Body.Close() 146 | body, _ := ioutil.ReadAll(rsp.Body) 147 | 148 | r := &xinge.CommonRsp{} 149 | json.Unmarshal(body, r) 150 | fmt.Printf("%+v", r) 151 | if r.RetCode != 0 { 152 | t.Errorf("Failed rsp=%+v", r) 153 | } 154 | ``` 155 | 156 | ### 多设备push 157 | ```go 158 | auther := auth.Auther{AppID: "AppID", SecretKey: "SecretKey"} 159 | pushReq, _ := req.NewPushReq( 160 | &xinge.Request{}, 161 | req.Platform(xinge.PlatformiOS), 162 | req.EnvDev(), 163 | req.AudienceType(xinge.AdTokenList), 164 | req.MessageType(xinge.MsgTypeNotify), 165 | req.TokenList([]string{"10000031", "10000034"}), 166 | req.PushID("0"), 167 | req.Message(xinge.Message{ 168 | Title: "haha", 169 | Content: "hehe", 170 | }), 171 | ) 172 | auther.Auth(pushReq) 173 | 174 | c := &http.Client{} 175 | rsp, _ := c.Do(pushReq) 176 | defer rsp.Body.Close() 177 | body, _ := ioutil.ReadAll(rsp.Body) 178 | 179 | r := &xinge.CommonRsp{} 180 | json.Unmarshal(body, r) 181 | fmt.Printf("%+v", r) 182 | if r.RetCode != 0 { 183 | t.Errorf("Failed rsp=%+v", r) 184 | } 185 | ``` 186 | 187 | ### 标签push 188 | ```go 189 | auther := auth.Auther{AppID: "AppID", SecretKey: "SecretKey"} 190 | pushReq, _ := req.NewPushReq( 191 | &xinge.Request{}, 192 | req.Platform(xinge.PlatformiOS), 193 | req.EnvDev(), 194 | req.AudienceType(xinge.AdTag), 195 | req.MessageType(xinge.MsgTypeNotify), 196 | req.TagList(&xinge.TagList{ 197 | Tags: []string{"new", "active"}, 198 | Operation: xinge.TagListOpAnd, 199 | }), 200 | req.PushID("0"), 201 | req.Message(xinge.Message{ 202 | Title: "haha", 203 | Content: "hehe", 204 | }), 205 | ) 206 | auther.Auth(pushReq) 207 | 208 | c := &http.Client{} 209 | rsp, _ := c.Do(pushReq) 210 | defer rsp.Body.Close() 211 | body, _ := ioutil.ReadAll(rsp.Body) 212 | 213 | r := &xinge.CommonRsp{} 214 | json.Unmarshal(body, r) 215 | fmt.Printf("%+v", r) 216 | if r.RetCode != 0 { 217 | t.Errorf("Failed rsp=%+v", r) 218 | } 219 | ``` 220 | 221 | ## 贡献代码指南 222 | 目前的设计是通过`ReqOpt`函数来扩展各种请求参数,尽量请保持代码风格一致,使用`gofmt`来格式化代码。 223 | 224 | 贡献代码时可先从项目中的`TODO`开始,同时也欢迎提交新feature的PR和bug issue。 225 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Auther 用来添加请求Authorization 10 | type Auther struct { 11 | AppID string 12 | SecretKey string 13 | } 14 | 15 | // Auth 添加一些默认的请求头 16 | func (a *Auther) Auth(req *http.Request) { 17 | // TODO: 全平台发送时如何填写Auth header 18 | req.Header.Add("Authorization", makeAuthHeader(a.AppID, a.SecretKey)) 19 | req.Header.Add("Content-Type", "application/json") 20 | } 21 | 22 | // makeAuthHeader 根据appid和secretKey拼接并base64encode 23 | func makeAuthHeader(appID, secretKey string) string { 24 | base64Str := base64.StdEncoding.EncodeToString( 25 | []byte( 26 | fmt.Sprintf("%s:%s", appID, secretKey), 27 | ), 28 | ) 29 | return fmt.Sprintf("Basic %s", base64Str) 30 | } 31 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // New 创建一个新的默认http客户端 9 | func New() *http.Client { 10 | return &http.Client{ 11 | Transport: &http.Transport{ 12 | MaxIdleConns: 100, 13 | MaxIdleConnsPerHost: 100, 14 | IdleConnTimeout: 30 * time.Second, 15 | DisableCompression: false, 16 | // 默认开启了keep-alive 17 | DisableKeepAlives: false, 18 | }, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /def.go: -------------------------------------------------------------------------------- 1 | package xinge 2 | 3 | // TODO: 信鸽人工服务 4 | // 后端API V3,iOS 发push的时候message_type填message的时候报错 5 | 6 | // 基础鉴权为未通过,请检查key[appKey|groupKey]是否与push_type匹配! 7 | 8 | // 另外由于iOS和Android是两个不同的appid和key,如果platform填all,即全平台推送的时候怎么填Authorization header? 9 | 10 | // CommonRspEnv 信鸽推送接口通用基础返回值的environment字段 11 | type CommonRspEnv string 12 | 13 | const ( 14 | // EnvProd 生产环境 15 | EnvProd CommonRspEnv = "product" 16 | // EnvDev 测试环境 17 | EnvDev CommonRspEnv = "dev" 18 | ) 19 | 20 | // CommonRsp 信鸽推送接口的通用基础返回值 21 | type CommonRsp struct { 22 | // TODO: doc this 23 | Seq int64 `json:"seq"` 24 | // 推送id 25 | PushID string `json:"push_id"` 26 | // 错误码 27 | RetCode int `json:"ret_code"` 28 | // 用户指定推送环境,仅支持iOS 29 | Environment CommonRspEnv `json:"environment"` 30 | // 结果描述 31 | ErrMsg string `json:"err_msg,omitempty"` 32 | // 请求正确且有额外数据时,则结果在这个字段中 33 | Result map[string]string `json:"result,omitempty"` 34 | } 35 | 36 | // AudienceType 推送目标 37 | type AudienceType string 38 | 39 | const ( 40 | // AdAll 全量推送 41 | AdAll AudienceType = "all" 42 | // AdTag 标签推送 43 | AdTag AudienceType = "tag" 44 | // AdToken 单设备推送 45 | AdToken AudienceType = "token" 46 | // AdTokenList 设备列表推送 47 | AdTokenList AudienceType = "token_list" 48 | // AdAccount 单账号推送 49 | AdAccount AudienceType = "account" 50 | // AdAccountList 账号列表推送 51 | AdAccountList AudienceType = "account_list" 52 | ) 53 | 54 | // Platform push API platform参数 55 | type Platform string 56 | 57 | const ( 58 | // PlatformAll 全平台推送 59 | PlatformAll Platform = "all" 60 | // PlatformAndroid 安卓推送 61 | PlatformAndroid Platform = "android" 62 | // PlatformiOS 苹果推送 63 | PlatformiOS Platform = "ios" 64 | ) 65 | 66 | // MessageType push API message_type参数 67 | type MessageType string 68 | 69 | const ( 70 | // MsgTypeNotify 消息类型为通知栏消息 71 | MsgTypeNotify MessageType = "notify" 72 | // MsgTypeMessage 消息类型为透传消息(Android)/静默消息(iOS) 73 | MsgTypeMessage MessageType = "message" 74 | ) 75 | 76 | // Request push API 必要参数 77 | type Request struct { 78 | // 受众类型,见AudienceType类型 79 | AudienceType AudienceType `json:"audience_type"` 80 | // 推送平台,见Platform类型 81 | Platform Platform `json:"platform"` 82 | // 消息内容 83 | Message Message `json:"message"` 84 | // 消息类型,见MessageType类型 85 | MessageType MessageType `json:"message_type"` 86 | 87 | // 当AudienceType == AdTag时必填 88 | TagList *TagList `json:"tag_list,omitempty"` 89 | // 当AudienceType == AdToken 或 AdTokenList 时必填的参数, 90 | // 当AdToken时即使传了多个token,也只有第一个会被推送 91 | // 当AdTokenList时,最多支持1000个token,同时push_id第一次请求时必须填0 92 | // 系统会返回一个push_id = 123(例),后续推送如果push_id填写123(例) 93 | // 则会使用跟123相同的文案推送 94 | TokenList []string `json:"token_list,omitempty"` 95 | // 当AudienceType == AdAccount 或 AdAccountList 时必填的参数, 96 | // 当AdAccount时即使传了多个token,也只有第一个会被推送 97 | // 当AdAccountList时,最多支持1000个token,同时push_id第一次请求时必须填0 98 | // 系统会返回一个push_id = 123(例),后续推送如果push_id填写123(例) 99 | // 则会使用跟123相同的文案推送 100 | AccountList []string `json:"account_list,omitempty"` 101 | 102 | // 消息离线存储时间(单位为秒) 103 | // 最长存储时间3天,若设置为0,则默认值(3天) 104 | // 建议取值区间[600, 86400x3] 105 | // 第三方通道离线保存消息不同厂商标准不同 106 | ExpireTime int `json:"expire_time,omitempty"` 107 | 108 | // 指定推送时间 109 | // 格式为yyyy-MM-DD HH:MM:SS 110 | // 若小于服务器当前时间,则会立即推送 111 | // 仅全量推送和标签推送支持此字段 112 | SendTime string `json:"send_time,omitempty"` 113 | 114 | // 多包名推送 115 | // 当app存在多个不同渠道包(例如应用宝、豌豆荚等),推送时如果是希望手机上安装任何一个渠道的app都能收到消息那么该值需要设置为true 116 | MultiPkg bool `json:"multi_pkg,omitempty"` 117 | 118 | // 循环任务重复次数 119 | // 仅支持全推、标签推 120 | // 建议取值[1, 15] 121 | LoopTimes int `json:"loop_times,omitempty"` 122 | 123 | // 用户指定推送环境,仅限iOS平台推送使用 124 | // product: 推送生产环境 125 | // dev: 推送开发环境 126 | Environment CommonRspEnv `json:"environment,omitempty"` 127 | 128 | // 统计标签,用于聚合统计 129 | // 使用场景(示例): 130 | // 现在有一个活动id:active_picture_123,需要给10000个设备通过单推接口(或者列表推送等推送形式)下发消息,同时设置该字段为active_picture_123 131 | // 推送完成之后可以使用v3统计查询接口,根据该标签active_picture_123 查询这10000个设备的实发、抵达、展示、点击数据 132 | StatTag string `json:"stat_tag,omitempty"` 133 | 134 | // 接口调用时,在应答包中信鸽会回射该字段,可用于异步请求 135 | // 使用场景:异步服务中可以通过该字段找到server端返回的对应应答包 136 | Seq int64 `json:"seq,omitempty"` 137 | 138 | // 单账号推送时可选 139 | // 账号类型,参考后面账号说明。 140 | // 必须与账号绑定时设定的账号类型一致 141 | AccountType int `json:"account_type,omitempty"` 142 | 143 | // 账号列表推送、设备列表推送时必需 144 | // 账号列表推送和设备列表推送时,第一次推送该值填0,系统会创建对应的推送任务, 145 | // 并且返回对应的pushid:123,后续推送push_id 填123(同一个文案) 146 | // 表示使用与123 id 对应的文案进行推送。(注:文案的有效时间由前面的expire_time 字段决定) 147 | PushID string `json:"push_id,omitempty"` 148 | } 149 | 150 | // TagListOperation 标签推送参数的逻辑操作符 151 | type TagListOperation string 152 | 153 | const ( 154 | // TagListOpAnd 推送tag1且tag2 155 | TagListOpAnd TagListOperation = "AND" 156 | // TagListOpOr 推送tag1或tag2 157 | TagListOpOr TagListOperation = "OR" 158 | ) 159 | 160 | // TagList 标签推送参数 161 | type TagList struct { 162 | // 标签 163 | Tags []string `json:"tags"` 164 | // 标签逻辑操作符 165 | Operation TagListOperation `json:"op"` 166 | } 167 | 168 | // Message 消息体 169 | type Message struct { 170 | Title string `json:"title,omitempty"` 171 | Content string `json:"content,omitempty"` 172 | AcceptTime []string `json:"accept_time,omitempty"` 173 | 174 | Android *AndroidParams `json:"android,omitempty"` 175 | 176 | IOS *IOSParams `json:"ios,omitempty"` 177 | } 178 | 179 | // AndroidParams 安卓push参数 180 | type AndroidParams struct { 181 | // TODO: doc these 182 | NID int `json:"n_id,omitempty"` 183 | BuilderID int `json:"builder_id,omitempty"` 184 | Ring int `json:"ring,omitempty"` 185 | RingRaw string `json:"ring_raw,omitempty"` 186 | Vibrate int `json:"vibrate,omitempty"` 187 | Lights int `json:"lights,omitempty"` 188 | Clearable int `json:"clearable,omitempty"` 189 | IconType int `json:"icon_type,omitempty"` 190 | IconRes string `json:"icon_res,omitempty"` 191 | StyleID int `json:"style_id,omitempty"` 192 | SmallIcon int `json:"small_icon,omitempty"` 193 | Action map[string]interface{} `json:"action,omitempty"` 194 | CustomContent map[string]string `json:"custom_content,omitempty"` 195 | } 196 | 197 | // IOSParams iOS push参数 198 | type IOSParams struct { 199 | Aps *Aps `json:"aps,omitempty"` 200 | Custom map[string]string `json:"custom,omitempty"` 201 | } 202 | 203 | // Aps 通知栏iOS消息的aps字段,详情请参照苹果文档 204 | type Aps struct { 205 | Alert map[string]string `json:"alert,omitempty"` 206 | Badge int `json:"badge,omitempty"` 207 | Category string `json:"category,omitempty"` 208 | ContentAvailable int `json:"content-available,omitempty"` 209 | Sound string `json:"sound,omitempty"` 210 | } 211 | 212 | // TODO: error type constant 213 | -------------------------------------------------------------------------------- /req/req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/FrontMage/xinge" 10 | ) 11 | 12 | // XingeURL 信鸽API地址 13 | var XingeURL = "https://openapi.xg.qq.com/v3/push/app" 14 | 15 | // URL 修改信鸽的请求URL 16 | func URL(url string) { 17 | XingeURL = url 18 | } 19 | 20 | // ReqOpt 请求选项修改函数 21 | type ReqOpt func(*xinge.Request) 22 | 23 | // NewSingleIOSAccountPush 新建一个iOS单账号push请求 24 | func NewSingleIOSAccountPush( 25 | account, title, content string, 26 | opts ...ReqOpt, 27 | ) (*http.Request, error) { 28 | req := &xinge.Request{ 29 | Platform: xinge.PlatformiOS, 30 | MessageType: xinge.MsgTypeNotify, 31 | AudienceType: xinge.AdAccount, 32 | AccountList: []string{account}, 33 | Message: xinge.Message{ 34 | Title: title, 35 | Content: content, 36 | IOS: &xinge.IOSParams{ 37 | Aps: &xinge.Aps{ 38 | Alert: map[string]string{ 39 | "title": title, 40 | "content": content, 41 | }, 42 | Badge: 1, 43 | Sound: "default", 44 | }, 45 | }, 46 | }, 47 | } 48 | return NewPushReq(req, opts...) 49 | } 50 | 51 | // NewSingleAndroidAccountPush 新建一个安卓通知栏push请求 52 | func NewSingleAndroidAccountPush( 53 | account, title, content string, 54 | opts ...ReqOpt, 55 | ) (*http.Request, error) { 56 | req := &xinge.Request{ 57 | Platform: xinge.PlatformAndroid, 58 | MessageType: xinge.MsgTypeNotify, 59 | AudienceType: xinge.AdAccount, 60 | AccountList: []string{account}, 61 | Message: xinge.Message{ 62 | Title: title, 63 | Content: content, 64 | }, 65 | } 66 | return NewPushReq(req, opts...) 67 | } 68 | 69 | // NewPushReq 新建一个push请求 70 | func NewPushReq(req *xinge.Request, opts ...ReqOpt) (request *http.Request, err error) { 71 | for _, opt := range opts { 72 | opt(req) 73 | } 74 | bodyBytes, err := json.Marshal(req) 75 | if err != nil { 76 | return nil, err 77 | } 78 | request, err = http.NewRequest("POST", XingeURL, bytes.NewReader(bodyBytes)) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return 83 | } 84 | 85 | // PlatformIOS 修改push平台为iOS 86 | func PlatformIOS() ReqOpt { 87 | return func(r *xinge.Request) { 88 | r.Platform = xinge.PlatformiOS 89 | } 90 | } 91 | 92 | // PlatformAndroid 修改push平台为PlatformAndroid 93 | func PlatformAndroid() ReqOpt { 94 | return func(r *xinge.Request) { 95 | r.Platform = xinge.PlatformAndroid 96 | } 97 | } 98 | 99 | // PlatformAll 修改push平台为全量推送 100 | func PlatformAll() ReqOpt { 101 | return func(r *xinge.Request) { 102 | r.Platform = xinge.PlatformAll 103 | } 104 | } 105 | 106 | // EnvProd 修改请求环境为product,只对iOS有效 107 | func EnvProd() ReqOpt { 108 | return func(r *xinge.Request) { 109 | r.Environment = xinge.EnvProd 110 | } 111 | } 112 | 113 | // EnvDev 修改请求环境为dev,只对iOS有效 114 | func EnvDev() ReqOpt { 115 | return func(r *xinge.Request) { 116 | r.Environment = xinge.EnvDev 117 | } 118 | } 119 | 120 | // Title 修改push title 121 | func Title(t string) ReqOpt { 122 | return func(r *xinge.Request) { 123 | r.Message.Title = t 124 | if r.Message.IOS != nil { 125 | if r.Message.IOS.Aps != nil { 126 | r.Message.IOS.Aps.Alert["title"] = t 127 | } else { 128 | r.Message.IOS.Aps = &xinge.Aps{ 129 | Alert: map[string]string{"title": t}, 130 | } 131 | } 132 | } else { 133 | r.Message.IOS = &xinge.IOSParams{ 134 | Aps: &xinge.Aps{ 135 | Alert: map[string]string{"title": t}, 136 | }, 137 | } 138 | } 139 | } 140 | } 141 | 142 | // Content 修改push content 143 | func Content(c string) ReqOpt { 144 | return func(r *xinge.Request) { 145 | r.Message.Content = c 146 | if r.Message.IOS != nil { 147 | if r.Message.IOS.Aps != nil { 148 | r.Message.IOS.Aps.Alert["body"] = c 149 | } else { 150 | r.Message.IOS.Aps = &xinge.Aps{ 151 | Alert: map[string]string{"body": c}, 152 | } 153 | } 154 | } else { 155 | r.Message.IOS = &xinge.IOSParams{ 156 | Aps: &xinge.Aps{ 157 | Alert: map[string]string{"body": c}, 158 | }, 159 | } 160 | } 161 | } 162 | } 163 | 164 | // TODO: accept_time modify 165 | 166 | // NID 修改nid 167 | func NID(id int) ReqOpt { 168 | return func(r *xinge.Request) { 169 | r.Message.Android.NID = id 170 | } 171 | } 172 | 173 | // BuilderID 修改builder_id 174 | func BuilderID(id int) ReqOpt { 175 | return func(r *xinge.Request) { 176 | r.Message.Android.BuilderID = id 177 | } 178 | } 179 | 180 | // Ring 修改ring 181 | func Ring(ring int) ReqOpt { 182 | return func(r *xinge.Request) { 183 | r.Message.Android.Ring = ring 184 | } 185 | } 186 | 187 | // RingRaw 修改ring_raw 188 | func RingRaw(rr string) ReqOpt { 189 | return func(r *xinge.Request) { 190 | r.Message.Android.RingRaw = rr 191 | } 192 | } 193 | 194 | // Vibrate 修改vibrate 195 | func Vibrate(v int) ReqOpt { 196 | return func(r *xinge.Request) { 197 | r.Message.Android.Vibrate = v 198 | } 199 | } 200 | 201 | // Lights 修改lights 202 | func Lights(l int) ReqOpt { 203 | return func(r *xinge.Request) { 204 | r.Message.Android.Lights = l 205 | } 206 | } 207 | 208 | // Clearable 修改clearable 209 | func Clearable(c int) ReqOpt { 210 | return func(r *xinge.Request) { 211 | r.Message.Android.Clearable = c 212 | } 213 | } 214 | 215 | // IconType 修改icon_type 216 | func IconType(it int) ReqOpt { 217 | return func(r *xinge.Request) { 218 | r.Message.Android.IconType = it 219 | } 220 | } 221 | 222 | // IconRes 修改icon_res 223 | func IconRes(ir string) ReqOpt { 224 | return func(r *xinge.Request) { 225 | r.Message.Android.IconRes = ir 226 | } 227 | } 228 | 229 | // StyleID 修改style_id 230 | func StyleID(s int) ReqOpt { 231 | return func(r *xinge.Request) { 232 | r.Message.Android.StyleID = s 233 | } 234 | } 235 | 236 | // SmallIcon 修改small_icon 237 | func SmallIcon(si int) ReqOpt { 238 | return func(r *xinge.Request) { 239 | r.Message.Android.SmallIcon = si 240 | } 241 | } 242 | 243 | // Action 修改action 244 | func Action(a map[string]interface{}) ReqOpt { 245 | return func(r *xinge.Request) { 246 | r.Message.Android.Action = a 247 | } 248 | } 249 | 250 | // AddAction 添加action 251 | func AddAction(k string, v interface{}) ReqOpt { 252 | return func(r *xinge.Request) { 253 | if r.Message.Android.Action == nil { 254 | r.Message.Android.Action = map[string]interface{}{k: v} 255 | } else { 256 | r.Message.Android.Action[k] = v 257 | } 258 | } 259 | } 260 | 261 | // CustomContent 修改custom_content 和 custom 262 | func CustomContent(ct map[string]string) ReqOpt { 263 | return func(r *xinge.Request) { 264 | r.Message.Android.CustomContent = ct 265 | r.Message.IOS.Custom = ct 266 | } 267 | } 268 | 269 | // CustomContentSet 设置custom_content和custom的某个字段 270 | func CustomContentSet(k, v string) ReqOpt { 271 | return func(r *xinge.Request) { 272 | if r.Message.Android.CustomContent == nil { 273 | r.Message.Android.CustomContent = map[string]string{k: v} 274 | } else { 275 | r.Message.Android.CustomContent[k] = v 276 | } 277 | if r.Message.IOS.Custom == nil { 278 | r.Message.IOS.Custom = map[string]string{k: v} 279 | } else { 280 | r.Message.IOS.Custom[k] = v 281 | } 282 | } 283 | } 284 | 285 | // Aps 修改aps 286 | func Aps(aps *xinge.Aps) ReqOpt { 287 | return func(r *xinge.Request) { 288 | r.Message.IOS.Aps = aps 289 | } 290 | } 291 | 292 | // AudienceType 修改audience_type 293 | func AudienceType(at xinge.AudienceType) ReqOpt { 294 | return func(r *xinge.Request) { 295 | r.AudienceType = at 296 | } 297 | } 298 | 299 | // Platform 修改platform 300 | func Platform(p xinge.Platform) ReqOpt { 301 | return func(r *xinge.Request) { 302 | r.Platform = p 303 | } 304 | } 305 | 306 | // Message 修改message 307 | func Message(m xinge.Message) ReqOpt { 308 | return func(r *xinge.Request) { 309 | r.Message = m 310 | } 311 | } 312 | 313 | // TagList 修改tag_list 314 | func TagList(tl *xinge.TagList) ReqOpt { 315 | return func(r *xinge.Request) { 316 | r.TagList = tl 317 | } 318 | } 319 | 320 | // TokenList 修改token_list 321 | func TokenList(tl []string) ReqOpt { 322 | return func(r *xinge.Request) { 323 | r.TokenList = tl 324 | } 325 | } 326 | 327 | // TokenListAdd 给token_list添加一个token 328 | func TokenListAdd(t string) ReqOpt { 329 | return func(r *xinge.Request) { 330 | if r.TokenList != nil { 331 | r.TokenList = append(r.TokenList, t) 332 | } else { 333 | r.TokenList = []string{t} 334 | } 335 | } 336 | } 337 | 338 | // AccountList 修改account_list 339 | func AccountList(al []string) ReqOpt { 340 | return func(r *xinge.Request) { 341 | r.AccountList = al 342 | } 343 | } 344 | 345 | // AccountListAdd 给account_list添加一个account 346 | func AccountListAdd(a string) ReqOpt { 347 | return func(r *xinge.Request) { 348 | if r.AccountList != nil { 349 | r.AccountList = append(r.AccountList, a) 350 | } else { 351 | r.AccountList = []string{a} 352 | } 353 | } 354 | } 355 | 356 | // ExpireTime 修改expire_time 357 | func ExpireTime(et time.Time) ReqOpt { 358 | return func(r *xinge.Request) { 359 | r.ExpireTime = int(et.Unix()) 360 | } 361 | } 362 | 363 | // SendTime 修改send_time 364 | func SendTime(st time.Time) ReqOpt { 365 | return func(r *xinge.Request) { 366 | r.SendTime = st.Format("2006-01-02 15:04:05:07") 367 | } 368 | } 369 | 370 | // MultiPkg 修改multi_pkg 371 | func MultiPkg(mp bool) ReqOpt { 372 | return func(r *xinge.Request) { 373 | r.MultiPkg = mp 374 | } 375 | } 376 | 377 | // LoopTimes 修改loop_times 378 | func LoopTimes(lt int) ReqOpt { 379 | return func(r *xinge.Request) { 380 | r.LoopTimes = lt 381 | } 382 | } 383 | 384 | // StatTag 修改stat_tag 385 | func StatTag(st string) ReqOpt { 386 | return func(r *xinge.Request) { 387 | r.StatTag = st 388 | } 389 | } 390 | 391 | // Seq 修改seq 392 | func Seq(s int64) ReqOpt { 393 | return func(r *xinge.Request) { 394 | r.Seq = s 395 | } 396 | } 397 | 398 | // AudienceType 修改account_type 399 | func AccountType(at int) ReqOpt { 400 | return func(r *xinge.Request) { 401 | r.AccountType = at 402 | } 403 | } 404 | 405 | // PushID 修改push_id 406 | func PushID(pid string) ReqOpt { 407 | return func(r *xinge.Request) { 408 | r.PushID = pid 409 | } 410 | } 411 | 412 | // MessageType 修改消息类型 413 | func MessageType(t xinge.MessageType) ReqOpt { 414 | return func(r *xinge.Request) { 415 | r.MessageType = t 416 | } 417 | } 418 | --------------------------------------------------------------------------------