├── .gitignore ├── README.md ├── client.go ├── client_test.go ├── constants.go ├── message.go └── result.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xiaomi-push 2 | 小米推送服务 Golang SDK 3 | 4 | Production ready, full golang implementation of Xiaomi Push API (http://dev.xiaomi.com/console/?page=appservice&mod=push) 5 | 6 | ```Go 7 | var client = xiaomipush.NewClient("yourappSecret", []string{"packageName"}) 8 | 9 | func main() { 10 | var msg1 *Message = xiaomipush.NewAndroidMessage("title", "body").SetPayload("this is payload1") 11 | client.Send(context.Background(), msg1, regID1) 12 | } 13 | 14 | ``` 15 | 16 | ### Sender APIs 17 | 18 | - [x] Send(msg *Message, regID string) 19 | - [x] SendToList(msg *Message, regIDList []string) 20 | - [x] SendTargetMessageList(msgList []*TargetedMessage) 21 | - [x] SendToAlias(msg *Message, alias string) 22 | - [x] SendToAliasList(msg *Message, aliasList []string) 23 | - [x] SendToUserAccount(msg *Message, userAccount string) 24 | - [x] SendToUserAccountList(msg *Message, accountList []string) 25 | - [x] Broadcast(msg *Message, topic string) 26 | - [x] BroadcastAll(msg *Message) (*SendResult, error) 27 | - [x] MultiTopicBroadcast(msg *Message, topics []string, topicOP TopicOP) 28 | - [x] CheckScheduleJobExist(msgID string) 29 | - [x] DeleteScheduleJob(msgID string) (*Result, error) 30 | - [x] DeleteScheduleJobByJobKey(jobKey string) (*Result, error) 31 | 32 | ### Stats APIs 33 | 34 | - [x] Stats(start, end, packageName string) 35 | - [x] GetMessageStatusByMsgID(msgID string) (*SingleStatusResult, error) 36 | - [x] GetMessageStatusByJobKey(jobKey string) (*BatchStatusResult, error) 37 | - [x] GetMessageStatusPeriod(beginTime, endTime int64) (*BatchStatusResult, error) 38 | 39 | ### Subscription APIs 40 | 41 | - [x] SubscribeTopicForRegID(regID, topic, category string) (*Result, error) 42 | - [x] SubscribeTopicForRegIDList(regIDList []string, topic, category string) (*Result, error) 43 | - [x] UnSubscribeTopicForRegID(regID, topic, category string) (*Result, error) 44 | - [x] UnSubscribeTopicForRegIDList(regIDList []string, topic, category string) (*Result, error) 45 | - [x] SubscribeTopicByAlias(aliases []string, topic, category string) (*Result, error) 46 | - [x] UnSubscribeTopicByAlias(aliases []string, topic, category string) (*Result, error) 47 | 48 | ### Feedback APIs 49 | 50 | - [x] GetInvalidRegIDs() (*InvalidRegIDsResult, error) 51 | 52 | ### DevTools APIs 53 | 54 | - [x] GetAliasesOfRegID(regID string) (*AliasesOfRegIDResult, error) 55 | - [x] GetTopicsOfRegID(regID string) (*TopicsOfRegIDResult, error) 56 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package xiaomipush 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | 13 | "golang.org/x/net/context" 14 | "golang.org/x/net/context/ctxhttp" 15 | ) 16 | 17 | type MiPush struct { 18 | packageName []string 19 | host string 20 | appSecret string 21 | } 22 | 23 | func NewClient(appSecret string, packageName []string) *MiPush { 24 | return &MiPush{ 25 | packageName: packageName, 26 | host: ProductionHost, 27 | appSecret: appSecret, 28 | } 29 | } 30 | 31 | //----------------------------------------Sender----------------------------------------// 32 | // 根据registrationId,发送消息到指定设备上 33 | func (m *MiPush) Send(ctx context.Context, msg *Message, regID string) (*SendResult, error) { 34 | params := m.assembleSendParams(msg, regID) 35 | bytes, err := m.doPost(ctx, m.host+RegURL, params) 36 | if err != nil { 37 | return nil, err 38 | } 39 | var result SendResult 40 | err = json.Unmarshal(bytes, &result) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &result, nil 45 | } 46 | 47 | // 根据regIds,发送消息到指定的一组设备上 48 | // regIds的个数不得超过1000个。 49 | func (m *MiPush) SendToList(ctx context.Context, msg *Message, regIDList []string) (*SendResult, error) { 50 | if len(regIDList) == 0 || len(regIDList) > 1000 { 51 | panic("wrong number regIDList") 52 | } 53 | return m.Send(ctx, msg, strings.Join(regIDList, ",")) 54 | } 55 | 56 | // 发送一组消息。其中TargetedMessage类中封装了Message对象和该Message所要发送的目标。注意:messages内所有TargetedMessage对象的targetType必须相同, 57 | // 不支持在一个调用中同时给regid和alias发送消息。 58 | // 如果是定时消息, 所有消息的time_to_send必须相同 59 | // 消息必须设置packagename, 见client_test TestMiPush_SendTargetMessageList 60 | func (m *MiPush) SendTargetMessageList(ctx context.Context, msgList []*TargetedMessage) (*SendResult, error) { 61 | if len(msgList) == 0 { 62 | return nil, errors.New("empty msg") 63 | } 64 | if len(msgList) == 1 { 65 | return m.Send(ctx, msgList[0].message, msgList[0].target) 66 | } 67 | params := m.assembleTargetMessageListParams(msgList) 68 | var bytes []byte 69 | var err error 70 | if msgList[0].targetType == TargetTypeRegID { 71 | bytes, err = m.doPost(ctx, m.host+MultiMessagesRegIDURL, params) 72 | } else if msgList[0].targetType == TargetTypeReAlias { 73 | bytes, err = m.doPost(ctx, m.host+MultiMessagesAliasURL, params) 74 | } else if msgList[0].targetType == TargetTypeAccount { 75 | bytes, err = m.doPost(ctx, m.host+MultiMessagesUserAccountURL, params) 76 | } else { 77 | panic("bad targetType") 78 | } 79 | 80 | if err != nil { 81 | return nil, err 82 | } 83 | var result SendResult 84 | err = json.Unmarshal(bytes, &result) 85 | if err != nil { 86 | return nil, err 87 | } 88 | return &result, nil 89 | } 90 | 91 | // 根据alias,发送消息到指定设备上 92 | func (m *MiPush) SendToAlias(ctx context.Context, msg *Message, alias string) (*SendResult, error) { 93 | params := m.assembleSendToAlisaParams(msg, alias) 94 | bytes, err := m.doPost(ctx, m.host+MessageAlisaURL, params) 95 | if err != nil { 96 | return nil, err 97 | } 98 | var result SendResult 99 | err = json.Unmarshal(bytes, &result) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return &result, nil 104 | } 105 | 106 | // 根据aliasList,发送消息到指定的一组设备上 107 | // 元素的个数不得超过1000个。 108 | func (m *MiPush) SendToAliasList(ctx context.Context, msg *Message, aliasList []string) (*SendResult, error) { 109 | if len(aliasList) == 0 || len(aliasList) > 1000 { 110 | panic("wrong number aliasList") 111 | } 112 | return m.SendToAlias(ctx, msg, strings.Join(aliasList, ",")) 113 | } 114 | 115 | // 根据account,发送消息到指定account上 116 | func (m *MiPush) SendToUserAccount(ctx context.Context, msg *Message, userAccount string) (*SendResult, error) { 117 | params := m.assembleSendToUserAccountParams(msg, userAccount) 118 | bytes, err := m.doPost(ctx, m.host+MessageUserAccountURL, params) 119 | if err != nil { 120 | return nil, err 121 | } 122 | var result SendResult 123 | err = json.Unmarshal(bytes, &result) 124 | if err != nil { 125 | return nil, err 126 | } 127 | return &result, nil 128 | } 129 | 130 | // 根据accountList,发送消息到指定的一组设备上 131 | // 元素的个数不得超过1000个。 132 | func (m *MiPush) SendToUserAccountList(ctx context.Context, msg *Message, accountList []string) (*SendResult, error) { 133 | if len(accountList) == 0 || len(accountList) > 1000 { 134 | panic("wrong number accountList") 135 | } 136 | return m.SendToUserAccount(ctx, msg, strings.Join(accountList, ",")) 137 | } 138 | 139 | // 根据topic,发送消息到指定一组设备上 140 | func (m *MiPush) Broadcast(ctx context.Context, msg *Message, topic string) (*SendResult, error) { 141 | params := m.assembleBroadcastParams(msg, topic) 142 | var bytes []byte 143 | var err error 144 | if len(m.packageName) > 1 { 145 | bytes, err = m.doPost(ctx, m.host+MultiPackageNameMessageMultiTopicURL, params) 146 | } else { 147 | bytes, err = m.doPost(ctx, m.host+MessageMultiTopicURL, params) 148 | } 149 | if err != nil { 150 | return nil, err 151 | } 152 | var result SendResult 153 | err = json.Unmarshal(bytes, &result) 154 | if err != nil { 155 | return nil, err 156 | } 157 | return &result, nil 158 | } 159 | 160 | // 向所有设备发送消息 161 | func (m *MiPush) BroadcastAll(ctx context.Context, msg *Message) (*SendResult, error) { 162 | params := m.assembleBroadcastAllParams(msg) 163 | var bytes []byte 164 | var err error 165 | if len(m.packageName) > 1 { 166 | bytes, err = m.doPost(ctx, m.host+MultiPackageNameMessageAllURL, params) 167 | } else { 168 | bytes, err = m.doPost(ctx, m.host+MessageAllURL, params) 169 | } 170 | if err != nil { 171 | return nil, err 172 | } 173 | var result SendResult 174 | err = json.Unmarshal(bytes, &result) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return &result, nil 179 | } 180 | 181 | type TopicOP string 182 | 183 | const ( 184 | UNION TopicOP = "UNION" // 并集 185 | INTERSECTION TopicOP = "INTERSECTION" // 交集 186 | EXCEPT TopicOP = "EXCEPT" // 差集 187 | ) 188 | 189 | // 向多个topic广播消息,支持topic间的交集、并集或差集(如果只有一个topic请用单topic版本) 190 | // TOPIC_OP是一个枚举类型,指定了发送广播消息时多个topic之间的运算关系。 191 | // 例如:topics的列表元素是[A, B, C, D],则并集结果是A∪B∪C∪D,交集的结果是A∩B∩C∩D,差集的结果是A-B-C-D 192 | func (m *MiPush) MultiTopicBroadcast(ctx context.Context, msg *Message, topics []string, topicOP TopicOP) (*SendResult, error) { 193 | if len(topics) > 5 || len(topics) == 0 { 194 | panic("topics size invalid") 195 | } 196 | if len(topics) == 1 { 197 | return m.Broadcast(ctx, msg, topics[0]) 198 | } 199 | params := m.assembleMultiTopicBroadcastParams(msg, topics, topicOP) 200 | bytes, err := m.doPost(ctx, m.host+MultiTopicURL, params) 201 | if err != nil { 202 | return nil, err 203 | } 204 | var result SendResult 205 | err = json.Unmarshal(bytes, &result) 206 | if err != nil { 207 | return nil, err 208 | } 209 | return &result, nil 210 | } 211 | 212 | // 检测定时消息的任务是否存在。 213 | // result.code = 0 为任务存在, 否则不存在 214 | func (m *MiPush) CheckScheduleJobExist(ctx context.Context, msgID string) (*Result, error) { 215 | params := m.assembleCheckScheduleJobParams(msgID) 216 | bytes, err := m.doPost(ctx, m.host+ScheduleJobExistURL, params) 217 | if err != nil { 218 | return nil, err 219 | } 220 | var result Result 221 | err = json.Unmarshal(bytes, &result) 222 | if err != nil { 223 | return nil, err 224 | } 225 | return &result, nil 226 | } 227 | 228 | // 删除指定的定时消息 229 | func (m *MiPush) DeleteScheduleJob(ctx context.Context, msgID string) (*Result, error) { 230 | params := m.assembleDeleteScheduleJobParams(msgID) 231 | bytes, err := m.doPost(ctx, m.host+ScheduleJobDeleteURL, params) 232 | if err != nil { 233 | return nil, err 234 | } 235 | var result Result 236 | err = json.Unmarshal(bytes, &result) 237 | if err != nil { 238 | return nil, err 239 | } 240 | return &result, nil 241 | } 242 | 243 | // 删除指定的定时消息 244 | func (m *MiPush) DeleteScheduleJobByJobKey(ctx context.Context, jobKey string) (*Result, error) { 245 | params := m.assembleDeleteScheduleJobByJobKeyParams(jobKey) 246 | bytes, err := m.doPost(ctx, m.host+ScheduleJobDeleteByJobKeyURL, params) 247 | if err != nil { 248 | return nil, err 249 | } 250 | var result Result 251 | err = json.Unmarshal(bytes, &result) 252 | if err != nil { 253 | return nil, err 254 | } 255 | return &result, nil 256 | } 257 | 258 | //----------------------------------------Stats----------------------------------------// 259 | // 获取指定日期范围内的日统计数据(如果日期范围包含今日,则今日数据为从今天00:00开始到现在的累计量)。 260 | // packageName: 261 | // Android设备,传入App的包名 262 | // IOS设备,传入App的Bundle Id 263 | func (m *MiPush) Stats(ctx context.Context, start, end, packageName string) (*StatsResult, error) { 264 | params := m.assembleStatsParams(start, end, packageName) 265 | bytes, err := m.doGet(ctx, m.host+StatsURL, params) 266 | if err != nil { 267 | return nil, err 268 | } 269 | var result StatsResult 270 | err = json.Unmarshal(bytes, &result) 271 | if err != nil { 272 | return nil, err 273 | } 274 | return &result, nil 275 | } 276 | 277 | //----------------------------------------Tracer----------------------------------------// 278 | // 获取指定ID的消息状态 279 | func (m *MiPush) GetMessageStatusByMsgID(ctx context.Context, msgID string) (*SingleStatusResult, error) { 280 | params := m.assembleStatusParams(msgID) 281 | bytes, err := m.doGet(ctx, m.host+MessageStatusURL, params) 282 | if err != nil { 283 | return nil, err 284 | } 285 | var result SingleStatusResult 286 | err = json.Unmarshal(bytes, &result) 287 | if err != nil { 288 | return nil, err 289 | } 290 | return &result, nil 291 | } 292 | 293 | // 获取某个时间间隔内所有消息的状态。 294 | func (m *MiPush) GetMessageStatusByJobKey(ctx context.Context, jobKey string) (*BatchStatusResult, error) { 295 | params := m.assembleStatusByJobKeyParams(jobKey) 296 | bytes, err := m.doGet(ctx, m.host+MessagesStatusURL, params) 297 | if err != nil { 298 | return nil, err 299 | } 300 | var result BatchStatusResult 301 | err = json.Unmarshal(bytes, &result) 302 | if err != nil { 303 | return nil, err 304 | } 305 | return &result, nil 306 | } 307 | 308 | // 获取某个时间间隔内所有消息的状态。 309 | func (m *MiPush) GetMessageStatusPeriod(ctx context.Context, beginTime, endTime int64) (*BatchStatusResult, error) { 310 | params := m.assembleStatusPeriodParams(beginTime, endTime) 311 | bytes, err := m.doGet(ctx, m.host+MessagesStatusURL, params) 312 | if err != nil { 313 | return nil, err 314 | } 315 | var result BatchStatusResult 316 | err = json.Unmarshal(bytes, &result) 317 | if err != nil { 318 | return nil, err 319 | } 320 | return &result, nil 321 | } 322 | 323 | //----------------------------------------Subscription----------------------------------------// 324 | 325 | // 给某个regid订阅标签 326 | func (m *MiPush) SubscribeTopicForRegID(ctx context.Context, regID, topic, category string) (*Result, error) { 327 | params := m.assembleSubscribeTopicForRegIDParams(regID, topic, category) 328 | bytes, err := m.doPost(ctx, m.host+TopicSubscribeURL, params) 329 | if err != nil { 330 | return nil, err 331 | } 332 | var result Result 333 | err = json.Unmarshal(bytes, &result) 334 | if err != nil { 335 | return nil, err 336 | } 337 | return &result, nil 338 | } 339 | 340 | // 给一组regid列表订阅标签 341 | func (m *MiPush) SubscribeTopicForRegIDList(ctx context.Context, regIDList []string, topic, category string) (*Result, error) { 342 | return m.SubscribeTopicForRegID(ctx, strings.Join(regIDList, ","), topic, category) 343 | } 344 | 345 | // 取消某个regid的标签。 346 | func (m *MiPush) UnSubscribeTopicForRegID(ctx context.Context, regID, topic, category string) (*Result, error) { 347 | params := m.assembleUnSubscribeTopicForRegIDParams(regID, topic, category) 348 | bytes, err := m.doPost(ctx, m.host+TopicUnSubscribeURL, params) 349 | if err != nil { 350 | return nil, err 351 | } 352 | var result Result 353 | err = json.Unmarshal(bytes, &result) 354 | if err != nil { 355 | return nil, err 356 | } 357 | return &result, nil 358 | } 359 | 360 | // 取消一组regid列表的标签 361 | func (m *MiPush) UnSubscribeTopicForRegIDList(ctx context.Context, regIDList []string, topic, category string) (*Result, error) { 362 | return m.UnSubscribeTopicForRegID(ctx, strings.Join(regIDList, ","), topic, category) 363 | } 364 | 365 | // 给一组alias列表订阅标签 366 | func (m *MiPush) SubscribeTopicByAlias(ctx context.Context, aliases []string, topic, category string) (*Result, error) { 367 | params := m.assembleSubscribeTopicByAliasParams(aliases, topic, category) 368 | bytes, err := m.doPost(ctx, m.host+TopicSubscribeByAliasURL, params) 369 | if err != nil { 370 | return nil, err 371 | } 372 | var result Result 373 | err = json.Unmarshal(bytes, &result) 374 | if err != nil { 375 | return nil, err 376 | } 377 | return &result, nil 378 | } 379 | 380 | // 取消一组alias列表的标签 381 | func (m *MiPush) UnSubscribeTopicByAlias(ctx context.Context, aliases []string, topic, category string) (*Result, error) { 382 | params := m.assembleUnSubscribeTopicByAliasParams(aliases, topic, category) 383 | bytes, err := m.doPost(ctx, m.host+TopicUnSubscribeByAliasURL, params) 384 | if err != nil { 385 | return nil, err 386 | } 387 | var result Result 388 | err = json.Unmarshal(bytes, &result) 389 | if err != nil { 390 | return nil, err 391 | } 392 | return &result, nil 393 | } 394 | 395 | //----------------------------------------Feedback----------------------------------------// 396 | 397 | // 获取失效的regId列表 398 | // 获取失效的regId列表,每次请求最多返回1000个regId。 399 | // 每次请求之后,成功返回的失效的regId将会从MiPush数据库删除。 400 | func (m *MiPush) GetInvalidRegIDs(ctx context.Context) (*InvalidRegIDsResult, error) { 401 | params := m.assembleGetInvalidRegIDsParams() 402 | bytes, err := m.doGet(ctx, InvalidRegIDsURL, params) 403 | if err != nil { 404 | return nil, err 405 | } 406 | var result InvalidRegIDsResult 407 | err = json.Unmarshal(bytes, &result) 408 | if err != nil { 409 | return nil, err 410 | } 411 | return &result, nil 412 | } 413 | 414 | //----------------------------------------DevTools----------------------------------------// 415 | 416 | // 获取一个应用的某个用户目前设置的所有Alias 417 | func (m *MiPush) GetAliasesOfRegID(ctx context.Context, regID string) (*AliasesOfRegIDResult, error) { 418 | params := m.assembleGetAliasesOfParams(regID) 419 | bytes, err := m.doGet(ctx, m.host+AliasAllURL, params) 420 | if err != nil { 421 | return nil, err 422 | } 423 | var result AliasesOfRegIDResult 424 | err = json.Unmarshal(bytes, &result) 425 | if err != nil { 426 | return nil, err 427 | } 428 | return &result, nil 429 | } 430 | 431 | // 获取一个应用的某个用户的目前订阅的所有Topic 432 | func (m *MiPush) GetTopicsOfRegID(ctx context.Context, regID string) (*TopicsOfRegIDResult, error) { 433 | params := m.assembleGetTopicsOfParams(regID) 434 | bytes, err := m.doGet(ctx, m.host+TopicsAllURL, params) 435 | if err != nil { 436 | return nil, err 437 | } 438 | var result TopicsOfRegIDResult 439 | err = json.Unmarshal(bytes, &result) 440 | if err != nil { 441 | return nil, err 442 | } 443 | return &result, nil 444 | } 445 | 446 | func (m *MiPush) assembleSendParams(msg *Message, regID string) url.Values { 447 | form := m.defaultForm(msg) 448 | form.Add("registration_id", regID) 449 | return form 450 | } 451 | 452 | func (m *MiPush) assembleTargetMessageListParams(msgList []*TargetedMessage) url.Values { 453 | form := url.Values{} 454 | type OneMsg struct { 455 | Target string `json:"target"` 456 | Message *Message `json:"message"` 457 | } 458 | var messages []*OneMsg 459 | 460 | for _, m := range msgList { 461 | messages = append(messages, &OneMsg{ 462 | Target: m.target, 463 | Message: m.message, 464 | }) 465 | } 466 | bytes, err := json.Marshal(messages) 467 | if err != nil { 468 | panic(err) 469 | } 470 | form.Add("messages", string(bytes)) 471 | form.Add("time_to_send", strconv.FormatInt(msgList[0].message.TimeToSend, 10)) 472 | return form 473 | } 474 | 475 | func (m *MiPush) assembleSendToAlisaParams(msg *Message, alias string) url.Values { 476 | form := m.defaultForm(msg) 477 | form.Add("alias", alias) 478 | return form 479 | } 480 | 481 | func (m *MiPush) assembleSendToUserAccountParams(msg *Message, userAccount string) url.Values { 482 | form := m.defaultForm(msg) 483 | form.Add("user_account", userAccount) 484 | return form 485 | } 486 | 487 | func (m *MiPush) assembleBroadcastParams(msg *Message, topic string) url.Values { 488 | form := m.defaultForm(msg) 489 | form.Add("topic", topic) 490 | return form 491 | } 492 | 493 | func (m *MiPush) assembleBroadcastAllParams(msg *Message) url.Values { 494 | form := m.defaultForm(msg) 495 | return form 496 | } 497 | 498 | func (m *MiPush) assembleMultiTopicBroadcastParams(msg *Message, topics []string, topicOP TopicOP) url.Values { 499 | form := m.defaultForm(msg) 500 | form.Add("topic_op", string(topicOP)) 501 | form.Add("topics", strings.Join(topics, ";$;")) 502 | return form 503 | } 504 | 505 | func (m *MiPush) assembleCheckScheduleJobParams(msgID string) url.Values { 506 | form := url.Values{} 507 | form.Add("job_id", msgID) 508 | return form 509 | } 510 | 511 | func (m *MiPush) assembleDeleteScheduleJobParams(msgID string) url.Values { 512 | form := url.Values{} 513 | form.Add("job_id", msgID) 514 | return form 515 | } 516 | 517 | func (m *MiPush) assembleDeleteScheduleJobByJobKeyParams(jobKey string) url.Values { 518 | form := url.Values{} 519 | form.Add("job_key", jobKey) 520 | return form 521 | } 522 | 523 | func (m *MiPush) assembleStatsParams(start, end, packageName string) string { 524 | form := url.Values{} 525 | form.Add("start_date", start) 526 | form.Add("end_date", end) 527 | form.Add("restricted_package_name", packageName) 528 | return "?" + form.Encode() 529 | } 530 | 531 | func (m *MiPush) assembleStatusParams(msgID string) string { 532 | form := url.Values{} 533 | form.Add("msg_id", msgID) 534 | return "?" + form.Encode() 535 | } 536 | 537 | func (m *MiPush) assembleStatusByJobKeyParams(jobKey string) string { 538 | form := url.Values{} 539 | form.Add("job_key", jobKey) 540 | return "?" + form.Encode() 541 | } 542 | 543 | func (m *MiPush) assembleStatusPeriodParams(beginTime, endTime int64) string { 544 | form := url.Values{} 545 | form.Add("begin_time", strconv.FormatInt(int64(beginTime), 10)) 546 | form.Add("end_time", strconv.FormatInt(int64(endTime), 10)) 547 | return "?" + form.Encode() 548 | } 549 | 550 | func (m *MiPush) assembleSubscribeTopicForRegIDParams(regID, topic, category string) url.Values { 551 | form := url.Values{} 552 | form.Add("registration_id", regID) 553 | form.Add("topic", topic) 554 | form.Add("restricted_package_name", strings.Join(m.packageName, ",")) 555 | if category != "" { 556 | form.Add("category", category) 557 | } 558 | return form 559 | } 560 | 561 | func (m *MiPush) assembleUnSubscribeTopicForRegIDParams(regID, topic, category string) url.Values { 562 | form := url.Values{} 563 | form.Add("registration_id", regID) 564 | form.Add("topic", topic) 565 | form.Add("restricted_package_name", strings.Join(m.packageName, ",")) 566 | if category != "" { 567 | form.Add("category", category) 568 | } 569 | return form 570 | } 571 | 572 | func (m *MiPush) assembleSubscribeTopicByAliasParams(aliases []string, topic, category string) url.Values { 573 | form := url.Values{} 574 | form.Add("aliases", strings.Join(aliases, ",")) 575 | form.Add("topic", topic) 576 | form.Add("restricted_package_name", strings.Join(m.packageName, ",")) 577 | if category != "" { 578 | form.Add("category", category) 579 | } 580 | return form 581 | } 582 | 583 | func (m *MiPush) assembleUnSubscribeTopicByAliasParams(aliases []string, topic, category string) url.Values { 584 | form := url.Values{} 585 | form.Add("aliases", strings.Join(aliases, ",")) 586 | form.Add("topic", topic) 587 | form.Add("restricted_package_name", strings.Join(m.packageName, ",")) 588 | if category != "" { 589 | form.Add("category", category) 590 | } 591 | return form 592 | } 593 | 594 | func (m *MiPush) assembleGetInvalidRegIDsParams() string { 595 | form := url.Values{} 596 | return "?" + form.Encode() 597 | } 598 | 599 | func (m *MiPush) assembleGetAliasesOfParams(regID string) string { 600 | form := url.Values{} 601 | form.Add("restricted_package_name", strings.Join(m.packageName, ",")) 602 | form.Add("registration_id", regID) 603 | return "?" + form.Encode() 604 | } 605 | 606 | func (m *MiPush) assembleGetTopicsOfParams(regID string) string { 607 | form := url.Values{} 608 | form.Add("restricted_package_name", strings.Join(m.packageName, ",")) 609 | form.Add("registration_id", regID) 610 | return "?" + form.Encode() 611 | } 612 | 613 | func (m *MiPush) doPost(ctx context.Context, url string, form url.Values) ([]byte, error) { 614 | var result []byte 615 | var req *http.Request 616 | var res *http.Response 617 | var err error 618 | req, err = http.NewRequest("POST", url, strings.NewReader(form.Encode())) 619 | if err != nil { 620 | panic(err) 621 | } 622 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") 623 | req.Header.Set("Authorization", "key="+m.appSecret) 624 | client := &http.Client{} 625 | tryTime := 0 626 | tryAgain: 627 | res, err = ctxhttp.Do(ctx, client, req) 628 | if err != nil { 629 | fmt.Println("xiaomi push post err:", err, tryTime) 630 | select { 631 | case <-ctx.Done(): 632 | return nil, err 633 | default: 634 | } 635 | tryTime += 1 636 | if tryTime < PostRetryTimes { 637 | goto tryAgain 638 | } 639 | return nil, err 640 | } 641 | if res.Body == nil { 642 | panic("xiaomi response is nil") 643 | } 644 | defer res.Body.Close() 645 | fmt.Println("res.StatusCode=", res.StatusCode) 646 | if res.StatusCode != http.StatusOK { 647 | return nil, errors.New("network error") 648 | } 649 | result, err = ioutil.ReadAll(res.Body) 650 | if err != nil { 651 | return nil, err 652 | } 653 | return result, nil 654 | } 655 | 656 | func (m *MiPush) doGet(ctx context.Context, url string, params string) ([]byte, error) { 657 | var result []byte 658 | var req *http.Request 659 | var res *http.Response 660 | var err error 661 | req, err = http.NewRequest("GET", url+params, nil) 662 | if err != nil { 663 | panic(err) 664 | } 665 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") 666 | req.Header.Set("Authorization", "key="+m.appSecret) 667 | 668 | client := &http.Client{} 669 | res, err = ctxhttp.Do(ctx, client, req) 670 | if res.Body == nil { 671 | panic("xiaomi response is nil") 672 | } 673 | defer res.Body.Close() 674 | if res.StatusCode != http.StatusOK { 675 | return nil, errors.New("network error") 676 | } 677 | result, err = ioutil.ReadAll(res.Body) 678 | if err != nil { 679 | return nil, err 680 | } 681 | return result, nil 682 | } 683 | 684 | func (m *MiPush) defaultForm(msg *Message) url.Values { 685 | form := url.Values{} 686 | if len(m.packageName) > 0 { 687 | form.Add("restricted_package_name", strings.Join(m.packageName, ",")) 688 | } 689 | if msg.TimeToLive > 0 { 690 | form.Add("time_to_live", strconv.FormatInt(msg.TimeToLive, 10)) 691 | } 692 | if len(msg.Payload) > 0 { 693 | form.Add("payload", msg.Payload) 694 | } 695 | if len(msg.Title) > 0 { 696 | form.Add("title", msg.Title) 697 | } 698 | if len(msg.Description) > 0 { 699 | form.Add("description", msg.Description) 700 | } 701 | form.Add("notify_type", strconv.FormatInt(int64(msg.NotifyType), 10)) 702 | form.Add("pass_through", strconv.FormatInt(int64(msg.PassThrough), 10)) 703 | if msg.NotifyID != 0 { 704 | form.Add("notify_id", strconv.FormatInt(int64(msg.NotifyID), 10)) 705 | } 706 | if msg.TimeToSend > 0 { 707 | form.Add("time_to_send", strconv.FormatInt(int64(msg.TimeToSend), 10)) 708 | } 709 | if len(msg.Extra) > 0 { 710 | for k, v := range msg.Extra { 711 | form.Add("extra."+k, v) 712 | } 713 | } 714 | return form 715 | } 716 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package xiaomipush 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "golang.org/x/net/context" 8 | ) 9 | 10 | var packageName string = "sbkssbkssbkssbkssbkssbkssbkssbks" 11 | 12 | var client = NewClient("sbkssbkssbkssbkssbkssbkssbkssbks", []string{packageName}) 13 | 14 | var msg1 *Message = NewAndroidMessage("hi baby1", "hi1").SetPayload("开心么1").SetPassThrough(0) 15 | var msg2 *Message = NewAndroidMessage("hi baby2", "hi2 ").SetPayload("开心么2").SetPassThrough(1) 16 | 17 | var regID1 string = "WFioJi0fiIco7vOrI4dnxxjeKAUqR7fjugoGkHUgxeo=" 18 | var regID2 string = "52Pe7fPIRXWsXhzn4eYJ1njYhBhN8Lcp8IJPOMjThdk=" 19 | 20 | var alias1 string = "alias1" 21 | var alias2 string = "alias2" 22 | 23 | var account1 string = "account1" 24 | var account2 string = "account2" 25 | 26 | var topic1 string = "topic1" 27 | var topic2 string = "topic2" 28 | 29 | func TestMiPush_Send(t *testing.T) { 30 | result, err := client.Send(context.Background(), msg1, regID1) 31 | if err != nil { 32 | t.Errorf("TestMiPush_Send failed :%v\n", err) 33 | } 34 | t.Logf("result=%#v\n", result) 35 | } 36 | 37 | func TestMiPush_SendWithTimeout(t *testing.T) { 38 | ctx, _ := context.WithTimeout(context.Background(), time.Second) 39 | time.Sleep(time.Second) 40 | _, err := client.Send(ctx, msg1, regID1) 41 | if err == nil { 42 | t.Errorf("TestMiPush_Send failed :%v\n", err) 43 | } else { 44 | t.Logf("err=%v\n", err) 45 | } 46 | } 47 | 48 | func TestMiPush_SendWithCancel(t *testing.T) { 49 | ctx, cancel := context.WithCancel(context.Background()) 50 | cancel() 51 | _, err := client.Send(ctx, msg1, regID1) 52 | if err == nil { 53 | t.Errorf("TestMiPush_Send failed :%v\n", err) 54 | } else { 55 | t.Logf("err=%v\n", err) 56 | } 57 | } 58 | 59 | func TestMiPush_SendToList(t *testing.T) { 60 | result, err := client.SendToList(context.TODO(), msg1, []string{regID1, regID2}) 61 | if err != nil { 62 | t.Errorf("TestMiPush_SendToList failed :%v\n", err) 63 | } 64 | t.Logf("result=%#v\n", result) 65 | } 66 | 67 | func TestMiPush_SendTargetMessageList(t *testing.T) { 68 | msgList := []*TargetedMessage{NewTargetedMessage(msg1.SetRestrictedPackageName(client.packageName), regID1, TargetTypeRegID), NewTargetedMessage(msg2.SetRestrictedPackageName(client.packageName), regID2, TargetTypeRegID)} 69 | result, err := client.SendTargetMessageList(context.TODO(), msgList) 70 | if err != nil { 71 | t.Errorf("TestMiPush_SendTargetMessageList failed :%v\n", err) 72 | } 73 | t.Logf("result=%#v\n", result) 74 | } 75 | 76 | func TestMiPush_SendToAlias(t *testing.T) { 77 | result, err := client.SendToAlias(context.TODO(), msg1, alias1) 78 | if err != nil { 79 | t.Errorf("TestMiPush_SendToAlias failed :%v\n", err) 80 | } 81 | t.Logf("result=%#v\n", result) 82 | } 83 | 84 | func TestMiPush_SendToAliasList(t *testing.T) { 85 | result, err := client.SendToAliasList(context.TODO(), msg1, []string{alias1, alias2}) 86 | if err != nil { 87 | t.Errorf("TestMiPush_SendToAliasList failed :%v\n", err) 88 | } 89 | t.Logf("result=%#v\n", result) 90 | } 91 | 92 | func TestMiPush_SendToUserAccount(t *testing.T) { 93 | result, err := client.SendToUserAccount(context.TODO(), msg1, account1) 94 | if err != nil { 95 | t.Errorf("TestMiPush_SendToUserAccount failed :%v\n", err) 96 | } 97 | t.Logf("result=%#v\n", result) 98 | } 99 | 100 | func TestMiPush_SendToUserAccountList(t *testing.T) { 101 | result, err := client.SendToUserAccountList(context.TODO(), msg1, []string{account1, account2}) 102 | if err != nil { 103 | t.Errorf("TestMiPush_SendToUserAccountList failed :%v\n", err) 104 | } 105 | t.Logf("result=%#v\n", result) 106 | } 107 | 108 | func TestMiPush_Broadcast(t *testing.T) { 109 | result, err := client.Broadcast(context.TODO(), msg1, topic1) 110 | if err != nil { 111 | t.Errorf("TestMiPush_Broadcast failed :%v\n", err) 112 | } 113 | t.Logf("result=%#v\n", result) 114 | } 115 | 116 | func TestMiPush_BroadcastAll(t *testing.T) { 117 | result, err := client.BroadcastAll(context.TODO(), msg1) 118 | if err != nil { 119 | t.Errorf("TestMiPush_BroadcastAll failed :%v\n", err) 120 | } 121 | t.Logf("result=%#v\n", result) 122 | } 123 | 124 | func TestMiPush_MultiTopicBroadcast(t *testing.T) { 125 | result, err := client.MultiTopicBroadcast(context.TODO(), msg1, []string{topic1, topic2}, INTERSECTION) 126 | if err != nil { 127 | t.Errorf("TestMiPush_MultiTopicBroadcast failed :%v\n", err) 128 | } 129 | t.Logf("result=%#v\n", result) 130 | } 131 | 132 | func TestMiPush_CheckScheduleJobExist(t *testing.T) { 133 | result, err := client.CheckScheduleJobExist(context.TODO(), "slm30b80474526081454i5") 134 | if err != nil { 135 | t.Errorf("TestMiPush_CheckScheduleJobExist failed :%v\n", err) 136 | } 137 | t.Logf("result=%#v\n", result) 138 | } 139 | 140 | func TestMiPush_DeleteScheduleJob(t *testing.T) { 141 | result, err := client.DeleteScheduleJob(context.TODO(), "slm30b80474526081454i5") 142 | if err != nil { 143 | t.Errorf("TestMiPush_DeleteScheduleJob failed :%v\n", err) 144 | } 145 | t.Logf("result=%#v\n", result) 146 | } 147 | 148 | func TestMiPush_DeleteScheduleJobByJobKey(t *testing.T) { 149 | result, err := client.DeleteScheduleJobByJobKey(context.TODO(), "Xcm45b21474513716292EL") 150 | if err != nil { 151 | t.Errorf("TestMiPush_DeleteScheduleJobByJobKey failed :%v\n", err) 152 | } 153 | t.Logf("result=%#v\n", result) 154 | } 155 | 156 | func TestMiPush_Stats(t *testing.T) { 157 | result, err := client.Stats(context.TODO(), "20160922", "20160922", packageName) 158 | if err != nil { 159 | t.Errorf("TestMiPush_Stats failed :%v\n", err) 160 | } 161 | t.Logf("result=%#v\n", result) 162 | } 163 | 164 | func TestMiPush_GetMessageStatusByMsgID(t *testing.T) { 165 | result, err := client.GetMessageStatusByMsgID(context.TODO(), "scm23b964745244861922w") 166 | if err != nil { 167 | t.Errorf("TestMiPush_GetMessageStatusByMsgID failed :%v\n", err) 168 | } 169 | t.Logf("result=%#v\n", result) 170 | } 171 | 172 | func TestMiPush_GetMessageStatusByJobKey(t *testing.T) { 173 | result, err := client.GetMessageStatusByJobKey(context.TODO(), "key111") 174 | if err != nil { 175 | t.Errorf("TestMiPush_GetMessageStatusByJobKey failed :%v\n", err) 176 | } 177 | t.Logf("result=%#v\n", result) 178 | } 179 | 180 | func TestMiPush_GetMessageStatusPeriod(t *testing.T) { 181 | result, err := client.GetMessageStatusPeriod(context.TODO(), time.Now().Add(-time.Hour*24).Unix()*1000, time.Now().Unix()*1000) 182 | if err != nil { 183 | t.Errorf("TestMiPush_GetMessageStatusPeriod failed :%v\n", err) 184 | } 185 | t.Logf("result=%#v\n", result) 186 | } 187 | 188 | //----------------------------------------Subscription----------------------------------------// 189 | 190 | func TestMiPush_SubscribeTopicForRegID(t *testing.T) { 191 | result, err := client.SubscribeTopicForRegID(context.TODO(), regID1, "topic3", "") 192 | if err != nil { 193 | t.Errorf("TestMiPush_SubscribeTopicForRegID failed :%v\n", err) 194 | } 195 | t.Logf("result=%#v\n", result) 196 | } 197 | 198 | func TestMiPush_SubscribeTopicForRegIDList(t *testing.T) { 199 | result, err := client.SubscribeTopicForRegIDList(context.TODO(), []string{regID1, regID2}, "topic5", "") 200 | if err != nil { 201 | t.Errorf("TestMiPush_SubscribeTopicForRegIDList failed :%v\n", err) 202 | } 203 | t.Logf("result=%#v\n", result) 204 | } 205 | 206 | func TestMiPush_UnSubscribeTopicForRegID(t *testing.T) { 207 | result, err := client.UnSubscribeTopicForRegID(context.TODO(), regID1, "topic3", "") 208 | if err != nil { 209 | t.Errorf("TestMiPush_UnSubscribeTopicForRegID failed :%v\n", err) 210 | } 211 | t.Logf("result=%#v\n", result) 212 | } 213 | 214 | func TestMiPush_UnSubscribeTopicForRegIDList(t *testing.T) { 215 | result, err := client.UnSubscribeTopicForRegIDList(context.TODO(), []string{regID1, regID2}, "topic5", "") 216 | if err != nil { 217 | t.Errorf("TestMiPush_SubscribeTopicForRegIDList failed :%v\n", err) 218 | } 219 | t.Logf("result=%#v\n", result) 220 | } 221 | 222 | func TestMiPush_SubscribeTopicByAlias(t *testing.T) { 223 | result, err := client.SubscribeTopicByAlias(context.TODO(), []string{alias1, alias2}, "topic5", "") 224 | if err != nil { 225 | t.Errorf("TestMiPush_SubscribeTopicByAlias failed :%v\n", err) 226 | } 227 | t.Logf("result=%#v\n", result) 228 | } 229 | 230 | func TestMiPush_UnSubscribeTopicByAlias(t *testing.T) { 231 | result, err := client.UnSubscribeTopicByAlias(context.TODO(), []string{alias1, alias2}, "topic5", "") 232 | if err != nil { 233 | t.Errorf("TestMiPush_SubscribeTopicByAlias failed :%v\n", err) 234 | } 235 | t.Logf("result=%#v\n", result) 236 | } 237 | 238 | //----------------------------------------Feedback----------------------------------------// 239 | 240 | func TestMiPush_GetInvalidRegIDs(t *testing.T) { 241 | result, err := client.GetInvalidRegIDs(context.TODO()) 242 | if err != nil { 243 | t.Errorf("TestMiPush_GetInvalidRegIDs failed :%v\n", err) 244 | } 245 | t.Logf("result=%#v\n", result) 246 | } 247 | 248 | //----------------------------------------DevTools----------------------------------------// 249 | 250 | func TestMiPush_GetAliasesOfRegID(t *testing.T) { 251 | result, err := client.GetAliasesOfRegID(context.TODO(), regID1) 252 | if err != nil { 253 | t.Errorf("TestMiPush_GetAliasesOfRegID failed :%v\n", err) 254 | } 255 | t.Logf("result=%#v\n", result) 256 | } 257 | 258 | func TestMiPush_GetTopicsOfRegID(t *testing.T) { 259 | result, err := client.GetTopicsOfRegID(context.TODO(), regID2) 260 | if err != nil { 261 | t.Errorf("TestMiPush_GetTopicsOfRegID failed :%v\n", err) 262 | } 263 | t.Logf("result=%#v\n", result) 264 | } 265 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package xiaomipush 2 | 3 | const ( 4 | ProductionHost = "https://api.xmpush.xiaomi.com" 5 | ) 6 | 7 | const ( 8 | RegURL = "/v3/message/regid" // 向某个regid或一组regid列表推送某条消息 9 | MultiMessagesRegIDURL = "/v2/multi_messages/regids" // 针对不同的regid推送不同的消息 10 | MultiMessagesAliasURL = "/v2/multi_messages/aliases" // 针对不同的aliases推送不同的消息 11 | MultiMessagesUserAccountURL = "/v2/multi_messages/user_accounts" // 针对不同的accounts推送不同的消息 12 | MessageAlisaURL = "/v3/message/alias" // 根据alias,发送消息到指定设备上 13 | MessageUserAccountURL = "/v2/message/user_account" // 根据account,发送消息到指定account上 14 | MultiPackageNameMessageMultiTopicURL = "/v3/message/multi_topic" // 根据topic,发送消息到指定一组设备上 15 | MessageMultiTopicURL = "/v2/message/topic" // 根据topic,发送消息到指定一组设备上 16 | MultiPackageNameMessageAllURL = "/v3/message/all" // 向所有设备推送某条消息 17 | MessageAllURL = "/v2/message/all" // 向所有设备推送某条消息 18 | MultiTopicURL = "/v3/message/multi_topic" // 向多个topic广播消息 19 | ScheduleJobExistURL = "/v2/schedule_job/exist" // 检测定时消息的任务是否存在。 20 | ScheduleJobDeleteURL = "/v2/schedule_job/delete" // 删除指定的定时消息。 21 | ScheduleJobDeleteByJobKeyURL = "/v3/schedule_job/delete" // 删除指定的定时消息。 22 | 23 | ) 24 | 25 | const ( 26 | StatsURL = "/v1/stats/message/counters" // 统计push 27 | MessageStatusURL = "/v1/trace/message/status" // 获取指定ID的消息状态 28 | MessagesStatusURL = "/v1/trace/messages/status" // 获取某个时间间隔内所有消息的状态 29 | ) 30 | 31 | const ( 32 | TopicSubscribeURL = "/v2/topic/subscribe" // 给某个regid订阅标签。 33 | TopicUnSubscribeURL = "/v2/topic/unsubscribe" // 取消某个regid的标签。 34 | TopicSubscribeByAliasURL = "/v2/topic/subscribe/alias" // 给一组alias列表订阅标签 35 | TopicUnSubscribeByAliasURL = "/v2/topic/unsubscribe/alias" // 取消一组alias列表的标签 36 | ) 37 | 38 | const ( 39 | InvalidRegIDsURL = "https://feedback.xmpush.xiaomi.com/v1/feedback/fetch_invalid_regids" 40 | ) 41 | 42 | const ( 43 | AliasAllURL = "/v1/alias/all" // 获取一个应用的某个用户目前设置的所有Alias 44 | TopicsAllURL = "/v1/topic/all" // 获取一个应用的某个用户的目前订阅的所有Topic 45 | ) 46 | 47 | var ( 48 | PostRetryTimes = 3 49 | ) 50 | 51 | // for future targeted push 52 | var ( 53 | BrandsMap = map[string]string{ 54 | "品牌": "MODEL", 55 | "小米": "xiaomi", 56 | "三星": "samsung", 57 | "华为": "huawei", 58 | "中兴": "zte", 59 | "中兴努比亚": "nubia", 60 | "酷派": "coolpad", 61 | "联想": "lenovo", 62 | "魅族": "meizu", 63 | "HTC": "htc", 64 | "OPPO": "oppo", 65 | "VIVO": "vivo", 66 | "摩托罗拉": "motorola", 67 | "索尼": "sony", 68 | "LG": "lg", 69 | "金立": "jinli", 70 | "天语": "tianyu", 71 | "诺基亚": "nokia", 72 | "美图秀秀": "meitu", 73 | "谷歌": "google", 74 | "TCL": "tcl", 75 | "锤子手机": "chuizi", 76 | "一加手机": "1+", 77 | "中国移动": "chinamobile", 78 | "昂达": "angda", 79 | "邦华": "banghua", 80 | "波导": "bird", 81 | "长虹": "changhong", 82 | "大可乐": "dakele", 83 | "朵唯": "doov", 84 | "海尔": "haier", 85 | "海信": "hisense", 86 | "康佳": "konka", 87 | "酷比魔方": "kubimofang", 88 | "米歌": "mige", 89 | "欧博信": "ouboxin", 90 | "欧新": "ouxin", 91 | "飞利浦": "philip", 92 | "维图": "voto", 93 | "小辣椒": "xiaolajiao", 94 | "夏新": "xiaxin", 95 | "亿通": "yitong", 96 | "语信": "yuxin", 97 | } 98 | 99 | PriceMap = map[string]string{ 100 | "0-999": "0-999", 101 | "1000-1999": "1000-1999", 102 | "2000-3999": "2000-3999", 103 | "4000+": "4000+", 104 | } 105 | ) 106 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package xiaomipush 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Message struct { 11 | RestrictedPackageName string `json:"restricted_package_name,omitempty"` // 设置app的多包名packageNames(多包名发送广播消息)。p 12 | Payload string `json:"payload,omitempty"` // 消息内容payload 13 | Title string `json:"title,omitempty"` // 通知栏展示的通知的标题 14 | Description string `json:"description,omitempty"` // 通知栏展示的通知的描述 15 | PassThrough int32 `json:"pass_through"` // 是否通过透传的方式送给app,1表示透传消息,0表示通知栏消息。 16 | NotifyType int32 `json:"notify_type,omitempty"` // DEFAULT_ALL = -1; DEFAULT_SOUND = 1; // 使用默认提示音提示 DEFAULT_VIBRATE = 2; // 使用默认震动提示 DEFAULT_LIGHTS = 4; // 使用默认led灯光提示 17 | TimeToLive int64 `json:"time_to_live,omitempty"` // 可选项。如果用户离线,设置消息在服务器保存的时间,单位:ms。服务器默认最长保留两周。 18 | TimeToSend int64 `json:"time_to_send,omitempty"` // 可选项。定时发送消息。timeToSend是以毫秒为单位的时间戳。注:仅支持七天内的定时消息。 19 | NotifyID int64 `json:"notify_id"` // 可选项。默认情况下,通知栏只显示一条推送消息。如果通知栏要显示多条推送消息,需要针对不同的消息设置不同的notify_id(相同notify_id的通知栏消息会覆盖之前的)。 20 | Extra map[string]string `json:"extra,omitempty"` // 可选项,对app提供一些扩展的功能,请参考2.2。除了这些扩展功能,开发者还可以定义一些key和value来控制客户端的行为。注:key和value的字符数不能超过1024,至多可以设置10个key-value键值对。 21 | } 22 | 23 | const ( 24 | MaxTimeToSend = time.Hour * 24 * 7 25 | MaxTimeToLive = time.Hour * 24 * 7 * 2 26 | ) 27 | 28 | func (m *Message) SetRestrictedPackageName(restrictedPackageNames []string) *Message { 29 | m.RestrictedPackageName = strings.Join(restrictedPackageNames, ",") 30 | return m 31 | } 32 | 33 | func (m *Message) SetPassThrough(passThrough int32) *Message { 34 | m.PassThrough = passThrough 35 | return m 36 | } 37 | 38 | func (m *Message) SetNotifyType(notifyType int32) *Message { 39 | m.NotifyType = notifyType 40 | return m 41 | } 42 | 43 | func (m *Message) SetTimeToSend(tts int64) *Message { 44 | if time.Since(time.Unix(0, tts*int64(time.Millisecond))) > MaxTimeToSend { 45 | m.TimeToSend = time.Now().Add(MaxTimeToSend).UnixNano() / 1e6 46 | } else { 47 | m.TimeToSend = tts 48 | } 49 | return m 50 | } 51 | 52 | func (m *Message) SetTimeToLive(ttl int64) *Message { 53 | if time.Since(time.Unix(0, ttl*int64(time.Millisecond))) > MaxTimeToLive { 54 | m.TimeToLive = time.Now().Add(MaxTimeToLive).UnixNano() / 1e6 55 | } else { 56 | m.TimeToLive = ttl 57 | } 58 | return m 59 | } 60 | 61 | func (m *Message) SetNotifyID(notifyID int64) *Message { 62 | m.NotifyID = notifyID 63 | return m 64 | } 65 | 66 | func (m *Message) EnableFlowControl() *Message { 67 | m.Extra["flow_control"] = "1" 68 | return m 69 | } 70 | 71 | func (m *Message) DisableFlowControl() *Message { 72 | delete(m.Extra, "flow_control") 73 | return m 74 | } 75 | 76 | // 开发者在发送消息时可以设置消息的组ID(JobKey),带有相同的组ID的消息会被聚合为一个消息组。 77 | // 系统支持按照消息组展示消息详情以及计划推送/送达数量/送达曲线等统计信息。 78 | // 另外,相同JobKey的消息在客户端会进行去重,只展示其中的第一条。 79 | // 这样如果发送时同JobKey中不慎有重复的设备也不用担心用户会收到重复的通知。 80 | func (m *Message) SetJobKey(jobKey string) *Message { 81 | m.Extra["jobkey"] = jobKey 82 | return m 83 | } 84 | 85 | // 小米推送服务器每隔1s将已送达或已点击的消息ID和对应设备的regid或alias通过调用第三方http接口传给开发者。 86 | func (m *Message) SetCallback(callbackURL string) *Message { 87 | m.Extra["callback"] = callbackURL 88 | m.Extra["callback.type"] = "3" // 1:送达回执, 2:点击回执, 3:送达和点击回执 89 | return m 90 | } 91 | 92 | // 添加自定义字段, 客户端使用 93 | func (m *Message) AddExtra(key, value string) *Message { 94 | m.Extra[key] = value 95 | return m 96 | } 97 | 98 | func (m *Message) JSON() []byte { 99 | bytes, err := json.Marshal(m) 100 | if err != nil { 101 | panic(err) 102 | } 103 | return bytes 104 | } 105 | 106 | //-----------------------------------------------------------------------------------// 107 | // 发送给Android设备的Message对象 108 | func NewAndroidMessage(title, description string) *Message { 109 | return &Message{ 110 | Payload: "", 111 | Title: title, 112 | Description: description, 113 | PassThrough: 0, 114 | NotifyType: -1, // default notify type 115 | TimeToLive: 0, 116 | TimeToSend: 0, 117 | NotifyID: 0, 118 | Extra: make(map[string]string), 119 | } 120 | } 121 | 122 | // 打开当前app对应的Launcher Activity。 123 | func (m *Message) SetLauncherActivity() *Message { 124 | m.Extra["notify_effect"] = "1" 125 | return m 126 | } 127 | 128 | // 打开当前app内的任意一个Activity。 129 | func (m *Message) SetJumpActivity(value string) *Message { 130 | m.Extra["notify_effect"] = "2" 131 | m.Extra["intent_uri"] = value 132 | return m 133 | } 134 | 135 | // 打开网页 136 | func (m *Message) SetJumpWebURL(value string) *Message { 137 | m.Extra["notify_effect"] = "3" 138 | m.Extra["web_uri"] = value 139 | return m 140 | } 141 | 142 | func (m *Message) SetPayload(payload string) *Message { 143 | m.Payload = payload 144 | return m 145 | } 146 | 147 | //-----------------------------------------------------------------------------------// 148 | // 发送给IOS设备的Message对象 149 | func NewIOSMessage(description string) *Message { 150 | return &Message{ 151 | Payload: "", 152 | Title: "", 153 | Description: description, 154 | PassThrough: 0, 155 | NotifyType: -1, // default notify type 156 | TimeToLive: 0, 157 | TimeToSend: 0, 158 | NotifyID: 0, 159 | Extra: make(map[string]string), 160 | } 161 | } 162 | 163 | // 可选项,自定义通知数字角标。 164 | func (i *Message) SetBadge(badge int64) *Message { 165 | i.Extra["badge"] = strconv.FormatInt(badge, 10) 166 | return i 167 | } 168 | 169 | // 可选项,iOS8推送消息快速回复类别。 170 | func (i *Message) SetCategory(category string) *Message { 171 | i.Extra["category"] = category 172 | return i 173 | } 174 | 175 | // 可选项,自定义消息铃声。 176 | func (i *Message) SetSoundURL(soundURL string) *Message { 177 | i.Extra["sound_url"] = soundURL 178 | return i 179 | } 180 | 181 | //-----------------------------------------------------------------------------------// 182 | // TargetedMessage封装了MiPush推送服务系统中的消息Message对象,和该Message对象所要发送到的目标。 183 | 184 | type TargetType int32 185 | 186 | const ( 187 | TargetTypeRegID TargetType = 1 188 | TargetTypeReAlias TargetType = 2 189 | TargetTypeAccount TargetType = 3 190 | ) 191 | 192 | type TargetedMessage struct { 193 | message *Message 194 | targetType TargetType 195 | target string 196 | } 197 | 198 | func NewTargetedMessage(m *Message, target string, targetType TargetType) *TargetedMessage { 199 | return &TargetedMessage{ 200 | message: m, 201 | targetType: targetType, 202 | target: target, 203 | } 204 | } 205 | 206 | func (tm *TargetedMessage) SetTargetType(targetType TargetType) *TargetedMessage { 207 | tm.targetType = targetType 208 | return tm 209 | } 210 | 211 | func (tm *TargetedMessage) SetTarget(target string) *TargetedMessage { 212 | tm.target = target 213 | return tm 214 | } 215 | 216 | func (tm *TargetedMessage) JSON() []byte { 217 | bytes, err := json.Marshal(tm) 218 | if err != nil { 219 | panic(err) 220 | } 221 | return bytes 222 | } 223 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package xiaomipush 2 | 3 | type Result struct { 4 | Result string `json:"result"` 5 | MessageID string `json:"trace_id"` 6 | Code int64 `json:"code"` 7 | Description string `json:"description,omitempty"` 8 | Info string `json:"info,omitempty"` 9 | Reason string `json:"reason,omitempty"` 10 | } 11 | 12 | type SendResult struct { 13 | Result 14 | Data struct { 15 | ID string `json:"id,omitempty"` 16 | } `json:"data,omitempty"` 17 | } 18 | 19 | type StatsResult struct { 20 | Result 21 | Data struct { 22 | Data []struct { 23 | Date string `json:"date"` 24 | AliasRecipients int64 `json:"alias_recipients"` 25 | UserAccountRecipients int64 `json:"useraccount_recipients"` 26 | RegIDRecipients int64 `json:"regid_recipients"` 27 | Received int64 `json:"received"` 28 | BroadcastRecipients int64 `json:"broadcast_recipients"` 29 | Click int64 `json:"click"` 30 | SingleRecipients int64 `json:"single_recipients"` 31 | } `json:"data,omitempty"` 32 | } `json:"data,omitempty"` 33 | } 34 | 35 | type SingleStatusResult struct { 36 | Result 37 | Data struct { 38 | Data struct { 39 | CreateTime string `json:"create_time"` 40 | CreateTimestamp int64 `json:"create_timestamp"` 41 | TimeToLive string `json:"time_to_live"` 42 | ClickRate string `json:"click_rate"` 43 | MsgType string `json:"msg_type"` 44 | DeliveryRate string `json:"delivery_rate"` 45 | Delivered int32 `json:"delivered"` 46 | ID string `json:"id"` 47 | Click int32 `json:"click"` 48 | Resolved int32 `json:"resolved"` 49 | } `json:"data,omitempty"` 50 | } `json:"data,omitempty"` 51 | } 52 | 53 | type BatchStatusResult struct { 54 | Result 55 | Data struct { 56 | Data []struct { 57 | CreateTime string `json:"create_time"` 58 | CreateTimestamp int64 `json:"create_timestamp"` 59 | TimeToLive string `json:"time_to_live"` 60 | ClickRate string `json:"click_rate"` 61 | MsgType string `json:"msg_type"` 62 | DeliveryRate string `json:"delivery_rate"` 63 | Delivered int32 `json:"delivered"` 64 | ID string `json:"id"` 65 | Click int32 `json:"click"` 66 | Resolved int32 `json:"resolved"` 67 | } `json:"data,omitempty"` 68 | } `json:"data,omitempty"` 69 | } 70 | 71 | type InvalidRegIDsResult struct { 72 | Result 73 | Data struct { 74 | List []string `json:"list,omitempty"` 75 | } `json:"data,omitempty"` 76 | } 77 | 78 | type AliasesOfRegIDResult struct { 79 | Result 80 | Data struct { 81 | List []string `json:"list,omitempty"` 82 | } `json:"data,omitempty"` 83 | } 84 | 85 | type TopicsOfRegIDResult struct { 86 | Result 87 | Data struct { 88 | List []string `json:"list,omitempty"` 89 | } `json:"data,omitempty"` 90 | } 91 | --------------------------------------------------------------------------------