├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── apns ├── AuthKey_PXA4CCXXXX.p8 ├── wfc.p12 └── wfc_voip.p12 ├── config ├── apns.properties ├── application.properties ├── fcm.properties ├── getui.properties ├── hm.properties ├── hms.properties ├── meizu.properties ├── oppo.properties ├── vivo.properties └── xiaomi.properties ├── fcm └── wildfirechat-4753c-firebase-adminsdk-1s9fh-6ac7fa7c6e.json ├── mvnw ├── mvnw.cmd ├── pom.xml ├── push.md └── src └── main ├── java └── cn │ └── wildfirechat │ └── push │ ├── PushApplication.java │ ├── PushController.java │ ├── PushMessage.java │ ├── PushMessageType.java │ ├── Utility.java │ ├── android │ ├── AndroidPushService.java │ ├── AndroidPushServiceImpl.java │ ├── AndroidPushType.java │ ├── fcm │ │ ├── FCMConfig.java │ │ └── FCMPush.java │ ├── getui │ │ ├── GetuiConfig.java │ │ └── GetuiPush.java │ ├── hms │ │ ├── HMSConfig.java │ │ └── HMSPush.java │ ├── meizu │ │ ├── MeiZuConfig.java │ │ └── MeiZuPush.java │ ├── oppo │ │ ├── OppoConfig.java │ │ └── OppoPush.java │ ├── vivo │ │ ├── VivoConfig.java │ │ └── VivoPush.java │ └── xiaomi │ │ ├── XiaomiConfig.java │ │ └── XiaomiPush.java │ ├── hm │ ├── HMConfig.java │ ├── HMPushService.java │ ├── HMPushServiceImpl.java │ └── payload │ │ ├── AlertPayload.java │ │ ├── VoipPayload.java │ │ └── internal │ │ ├── ClickAction.java │ │ ├── Notification.java │ │ ├── Payload.java │ │ └── Target.java │ └── ios │ ├── ApnsConfig.java │ ├── ApnsServer.java │ ├── IOSPushService.java │ ├── IOSPushServiceImpl.java │ └── IOSPushType.java ├── libs ├── MiPush_SDK_Server_2_2_19.jar ├── httpclient-4.5.jar ├── httpcore-4.4.1.jar ├── opush-server-sdk-1.0.4.jar └── vPush-server-sdk-1.0.jar └── resources └── application.properties /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # 提issue要求 2 | 请仔细阅读[推送项目说明](https://github.com/wildfirechat/push_server/blob/master/README.md)和[接入推送流程](https://github.com/wildfirechat/push_server/blob/master/push.md)。正常情况下都可以接入成功的,如果无法接入成功,请提供一下全部信息: 3 | 4 | 1. 请写出推送的完整流程。 5 | 2. 请写出推送的每个环节是那一方来负责,比如android端注册推送并调用setDeviceToken由你们android研发来负责。 6 | 3. 你认为那个环节出了问题,并给出证据。因为我们没有服务和客户端的任何信息,所以请务必给出全面详细的信息。 7 | 4. ```接入推送流程```中问题排查中每一步的结果。 8 | 9 | 为了尽快定位问题,需要提供足够的信息,请按照我们的要求格式来提问。***如果您不按照格式提问,我们将无法回复您的问题*** 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | push.iml 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 wildfirechat 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 | 1. ikidou/TypeBuilder 24 | Copyright 2016 ikidou 25 | 26 | Licensed under the Apache License, Version 2.0 (the "License"); 27 | you may not use this file except in compliance with the License. 28 | You may obtain a copy of the License at 29 | 30 | http://www.apache.org/licenses/LICENSE-2.0 31 | 32 | Unless required by applicable law or agreed to in writing, software 33 | distributed under the License is distributed on an "AS IS" BASIS, 34 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 35 | See the License for the specific language governing permissions and 36 | limitations under the License. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 野火IM 推送服务 2 | 作为野火IM的推送服务的演示,支持小米、华为、魅族、OPPO、Vivo、苹果apns和谷歌FCM。并且可以添加更多的推送厂商和自定义推送模式。 3 | 4 | ## 工作原理 5 | 推送功能对于所有IM来说都是非常重要的功能,然而手机系统又没有统一的推送服务,对接起来难度很大。另外一方面客户有不同对接需求,有的要求使用第三方,有的要求使用厂商推送,有的需要在海外添加谷歌推送,有的对推送的格式有不同的要求。 6 | 7 | 为了满足各种各样的需求,提供足够好的灵活性,野火IM把推送子系统独立出来,客户只要理解了推送子系统运行的原理,就能做好各种自定义处理。 8 | ![架构图](https://docs.wildfirechat.cn/architecture/wildfire_architecture.png) 9 | > 如果架构图无法查看,可以点击[这里](https://docs.wildfirechat.cn/architecture/wildfire_architecture.png)查看 10 | 11 | 图中紫色部分为推送子系统,推送子系统的所有源码都是开源的,且可以随意修改。推送子系统的工作流程如下: 12 | 1. 应用启动后,推送SDK初始化,判断采用那种推送服务,比如华为手机就用华为推送,小米手机就用小米推送,或者全部或部分使用第三方推送。如果客户要加其它推送也是在这里加。选定好推送厂商后,就初始化对应推送厂商的SDK,注册成功后会得到推送token,调用IM SDK的setDeviceToken,传入推送token和类型。注意类型是可以扩展的,而且对IM系统没有任何影响的。 13 | 2. SDK被调用setDeviceToken后,会把推送token和类型传入到IM服务,IM服务为对应手机保存下来以备后用。事实上IM服务不需要理解token和type的含义,只需要透传给推送服务即可。 14 | 3. IM服务处理消息时发现用户不在线或者下发消息失败,则会启动是否要推送的决策,比如消息是否需要推送(预制消息已经支持,自定义消息需要传入push content),用户是否全局静音,会话是否被静音,客户有多少天没有登录(超过7天没登录就不推送)。达到推送条件后,跟把所有推送需要的内容打包发给推送服务。 15 | 4. 推送服务接收到IM服务的请求,把推送数据放到消费队列中并立即返回(IM服务不能被阻塞),然后逐步处理推送事件。每个推送事件中都包含了所有需要处理的数据,其中包括1步骤中的推送Token和类型,然后根据类型来调用对用推送厂商的服务,比如华为/小米/苹果/第三方厂商/谷歌/OPPO/Vivo等,调用他们的SDK进行推送。 16 | 5. 系统厂商或第三方推送厂商利用他们的通道推送到客户端,一般有2种形式:一种是通知栏,不激活应用只弹出通知栏;另外一种形式是透传,把应用激活并把数据传递到客户端的推送相关代码种,应用激活后有一段活跃时间,在这个活跃时间连接IM服务,接收下来消息,并弹出本地通知。 17 | 18 | ## 通知类型 19 | 一般情况下有2种推送,一种是本地通知,另外一种是远程推送。 20 | 1. 本地通知:指应用在后台处于激活状态,当有此用户的新消息时,消息会被收下来,然后本地弹出通知。 21 | 2. 远程推送:指应用处于冻结或者杀死状态,当有此用户的新消息时,消息无法被收下来,需要借助推送服务通知到用户。 22 | 23 | 本地通知和远程推送在手机上的表现很接近,都是应用放到后台,然后有人给此账号发送消息,通知栏弹出通知。实际上处理流程完全不同。本项目处理的是远程推送。***在处理通知问题时,首先要确认的是本地通知还是远程推送***。 24 | 25 | ## 接入推送 26 | 接入推送并不是简单得将推送服务跑起来即可,请详细阅读[接入推送流程](./push.md) 27 | 28 | ## 添加其它推送服务 29 | 由前面的介绍可以看出,推送子服务是独立于IM服务,而且客户端和服务器部分都是开源的,而且考虑到了扩展性,可以很容易地添加其它推送类型。具体步骤如下: 30 | 1. 必须理解推送的工作原理,知道流程是:客户端注册推送-》客户端注册推送成功得到deviceToken-》客户端调用设置deviceToken和类型,这两个数据被存储到IM服务。当IM服务需要推送时,IM服务打包推送信息(包括deviceToken和类型)请求到推送服务-》推送服务根据类型选择服务商推送数据。 31 | 2. 客户端扩展一个新的推送类型。 32 | 3. 客户端在应用启动时,添加处理这种推送类型的注册 33 | 4. 在注册成功后会得到deviceToken,调用IM SDK的setDeviceToken接口传人deviceToken和类型。 34 | 5. 推送服务添加对这种类型的处理。 35 | 36 | ## 接入个推或者极光 37 | 我们提供有个推的分支,切换过去,然后按照个推文档申请推送功能。但要注意只有开通厂商推送功能才可以真的做到离线推送。极光推送可以参考个推或者上面添加其他推送服务的说明来添加,注意同样需要开通厂商推送来实现离线推送。 38 | 39 | ## 使用到的开源代码 40 | 1. [TypeBuilder](https://github.com/ikidou/TypeBuilder) 一个用于生成泛型的简易Builder 41 | 42 | ## LICENSE 43 | UNDER MIT LICENSE. 详情见LICENSE文件 44 | -------------------------------------------------------------------------------- /apns/AuthKey_PXA4CCXXXX.p8: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGXAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgGyCRn7aWnLChrRxF 3 | 2Xf0PN7BOMIF4EQth/OynvzXY0ygCgYIKoZIzj0DAQehRANCAAQ9vdBVqZfAvFi+ 4 | WSWiF1I9V54lt377jAPddY53Gd3f49imoE39C001TOhbso7t4U5+1m5gar6Y80fZ 5 | w1n01kI1 6 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /apns/wfc.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/push_server/554618ab8cf9af501c47eea7a2f8999ae8f572ce/apns/wfc.p12 -------------------------------------------------------------------------------- /apns/wfc_voip.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/push_server/554618ab8cf9af501c47eea7a2f8999ae8f572ce/apns/wfc_voip.p12 -------------------------------------------------------------------------------- /config/apns.properties: -------------------------------------------------------------------------------- 1 | # p12证书和p8证书选一个就行。如果选p12证书,请把 apns.auth_key_path的这几行删掉。 2 | 3 | # p12证书配置。 4 | apns.cer_path=apns/wfc.p12 5 | apns.cer_pwd=123456 6 | apns.voip_cer_path=apns/wfc_voip.p12 7 | apns.voip_cer_pwd=123456 8 | 9 | # p8证书配置 10 | apns.auth_key_path=apns/AuthKey_PXA4CCXXXX.p8 11 | apns.key_id=PXA4CCXXXX 12 | apns.team_id=Y8356MXXXX 13 | 14 | # 铃声配置 15 | apns.alert=default 16 | apns.voip_alert=ring.caf 17 | 18 | # 苹果要求使用voip推送必须使用callkit,不然会停掉voip推送。由于大陆政策,callkit被禁止,所以在大陆无法使用voip推送。 19 | # 苹果政策参考 https://developer.apple.com/documentation/pushkit/pkpushregistrydelegate/2875784-pushregistry?language=objc 20 | # On iOS 13.0 and later, if you fail to report a call to CallKit, the system will terminate your app. Repeatedly failing to report calls may cause the system to stop delivering any more VoIP push notifications to your app. If you want to initiate a VoIP call without using CallKit, register for push notifications using the UserNotifications framework instead of PushKit. For more information, see UserNotifications. 21 | apns.voip_feature=false 22 | -------------------------------------------------------------------------------- /config/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8085 -------------------------------------------------------------------------------- /config/fcm.properties: -------------------------------------------------------------------------------- 1 | fcm.credentialsPath=fcm/wildfirechat-4753c-firebase-adminsdk-1s9fh-6ac7fa7c6e.json -------------------------------------------------------------------------------- /config/getui.properties: -------------------------------------------------------------------------------- 1 | getui.appId= 2 | getui.appKey= 3 | getui.masterSecret= 4 | getui.domain=https://restapi.getui.com/v2/ -------------------------------------------------------------------------------- /config/hm.properties: -------------------------------------------------------------------------------- 1 | # 请参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/push-server-intro-0000001862404301-V5 2 | #请在代码中加密私钥,仅获取 '-----BEGIN PRIVATE KEY-----\n' 和 '\n-----END PRIVATE KEY-----\n'之间的字符 3 | hm.privateKey=MIIJQgIBADANBgkqhki****== 4 | # 请使用服务帐号密钥文件中的sub_account替换 5 | hm.iss=1002*** 6 | # 请使用服务帐号密钥文件中的key_id替换 7 | hm.kid=184d3688732245d*** 8 | # 项目 id 9 | hm.projectId=38842184*** 10 | 11 | # 音视频通话推送 需要申请特别的推送权限,请参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/push-apply-right-V5#section7291115452410 12 | # 只有支持 voip push 之后, 才能接入 callkit,实现后台弹出音视频通话邀请横幅 13 | # 不支持 voip push 时,将在通知栏显示一条 xxx邀请你进行音视频通话的提示 14 | hm.supportVoipPush=false 15 | 16 | -------------------------------------------------------------------------------- /config/hms.properties: -------------------------------------------------------------------------------- 1 | hms.appSecret=a4e5e6a0c8a5d8424aba5a8f0aae3d0c 2 | hms.appId=100221325 -------------------------------------------------------------------------------- /config/meizu.properties: -------------------------------------------------------------------------------- 1 | meizu.appSecret=098f6939499a44328fe201eb82d01fb2 2 | meizu.appId=113616 -------------------------------------------------------------------------------- /config/oppo.properties: -------------------------------------------------------------------------------- 1 | oppo.AppKey=16c6afe503b24259928e082ef01a6bf2 2 | oppo.AppSecret=2114e4067de4424fbfc0638e311ce88c -------------------------------------------------------------------------------- /config/vivo.properties: -------------------------------------------------------------------------------- 1 | vivo.appSecret=d0f24e5b-e92b-4b95-8d45-927bec3ba512 2 | vivo.appId=12918 3 | vivo.appKey=c42feb05-de6c-427d-af55-4f902d9e0a75 -------------------------------------------------------------------------------- /config/xiaomi.properties: -------------------------------------------------------------------------------- 1 | xiaomi.appSecret=66nAHUMwmGz042clVI5bVg== 2 | xiaomi.channel_id=12720000 -------------------------------------------------------------------------------- /fcm/wildfirechat-4753c-firebase-adminsdk-1s9fh-6ac7fa7c6e.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "wildfirechat-4753c", 4 | "private_key_id": "6ac7fa7c6e24a7625ac2f676246be5d5042f6c2b", 5 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3pvIgXcQ9Ac5V\n9I+F+T2CmJh+Bn9B3UDh0PXvQMwRoY6+r4xYv4KdzJ0c3f4zumfsCC0hf2O8D2VS\nRnHrNH69dlXAq0ZvfKSMvTrbE21uYHYEp0H0Jtxj/swIfu7A5RTWvgHfy9pS8tWr\njfr7N9JQsJBcMSpeqDb/ls0bo9xy1Ipa1EgLEzg4CfFJESt5LIQNfaPVl6HbTOmY\n5HKSxluridbajC6IyLYoI2vmry1JfNioSCCOikKgX9rOI4x0lCBJSr6phc6juEky\nTntUyrpAGnOUHEwxVO8ONtjfopB71bF6/LLUMZsaunHBAcpPtcJAGZOu94/VQtTX\nX+1xTIXhAgMBAAECggEAG600mw0WeE0v38NJ59pOW0KM5J0tC2uBDib0ETjWKCgN\nP45sG6nM0OQXn1STLQ9//tGin4Q3rw0w60vDejtAiGPve2g0ZrPFhpIz7vUIDlTK\nJFxic51xpD4vWG5so7RCRjR/Ss9JWSvQaJiuhLJ+Zvuc3c/o/zfhhfYfdfVlUQaV\n3OpNLXC3kML/yWQ+YdzMdaqjS3wwQcQL+aTdJFke7t2XHr9c34o4WK/JS6WBfo28\ncn0qTm0uFFF6QaroJU4TAJiEyY/0joSvXX+nzcH/stybDrwya3UOmf53v9HiAtBd\nZVnSyzpAOfSoBtGEy7Zo5+/i1l5F9RBCiJ1NLqIuWQKBgQD31dqfiyi5Yv254fxV\nWYJvi53zHoF0ANjy57bz6aAlyPkwO52k/ZPk2+0MUgtzmJP3LdpN8QTO/l62mprR\nKY9HKG3vUWV6PzU6w2LQ8eeFzaAUlM81lGnpUECaa3OO53mtyPc4pdZJ2b1eo9hN\njjgqrF45tnaFEmEtERs7m1lItQKBgQC9s8uvvPKYZyhgmwL1GHDhB+d3osqnk0td\n0sLqtAdlq0ov+chACW9SfGhLxDVGrpV4Ro9XvRekFyzDVYYrnMPpCfYpr8t20LI7\n58XGhlnyL59FPJ8bx6/VE2E8Yr+4+OF/YW4ZnUfaQyFJF9w3bS4a4R8ppicrJxU/\nMKlmSxLf/QKBgQD2c4hDTWwC5qivd0WpTiiCahF42WYcCFe1Pa1WYoWp7W+3giEj\nGDNAy16v+MqAekLx83v6M+n3OUbQSXAY/T4IofCoooXKCh8Rv4h9hYEZMsC0lsJz\nYpHrvK1xoda5TgBXS9hkUa2FpOxGt6H6hane7aeJtqOncv6FhVRScwpXXQKBgEZP\nNzUhUMDPqxVzHnt83YlqBo4+1eGaJBrYHMokg4FZJRv29hNV696koXtDc4OI/Xkg\nncwlF3gH5t1W+216otniiUwWDdExtH2jf5f+/6NVpzBgMZB4SGEu16Er8Gc8R0eQ\n8t+nfZQVwWZ343TfkHEB8yzamjXSPHu5K2/wb0R5AoGBANLagvM9/xWM+zccbDeb\nFFDWczBD7eQ+QbKdr10yKd/yUzUqngzIDyFtCX1Z8SYOuD1jMLCnGQVLcjfbJcm1\nX8bV+9F7mEIJqUAyAZaoLT7/VZH7RKMWJwwY2y3nL7Go9Ijt+PCTmefxbjFAa51w\nJnPt4fNNT6ftnbXsf7VzeA8/\n-----END PRIVATE KEY-----\n", 6 | "client_email": "firebase-adminsdk-1s9fh@wildfirechat-4753c.iam.gserviceaccount.com", 7 | "client_id": "114736707742305810727", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://oauth2.googleapis.com/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-1s9fh%40wildfirechat-4753c.iam.gserviceaccount.com" 12 | } 13 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | cn.wildfirechat 7 | push 8 | 0.1.2 9 | jar 10 | 11 | push 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.6.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 2.17.1 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-web 37 | 38 | 39 | 40 | com.xiaomi.push 41 | mipush-sdk-server 42 | 2.2.18 43 | system 44 | ${project.basedir}/src/main/libs/MiPush_SDK_Server_2_2_19.jar 45 | 46 | 47 | 48 | 49 | org.apache.logging.log4j 50 | log4j-slf4j-impl 51 | ${log4j2.version} 52 | 53 | 54 | org.apache.logging.log4j 55 | log4j-api 56 | ${log4j2.version} 57 | 58 | 59 | org.apache.logging.log4j 60 | log4j-core 61 | ${log4j2.version} 62 | 63 | 64 | org.apache.logging.log4j 65 | log4j-to-slf4j 66 | ${log4j2.version} 67 | 68 | 69 | 70 | com.oppo.push 71 | opush-server 72 | 1.0.4 73 | system 74 | ${project.basedir}/src/main/libs/opush-server-sdk-1.0.4.jar 75 | 76 | 77 | 78 | 79 | com.meizu.flyme 80 | push-server-sdk 81 | 1.2.7.20180307_release 82 | 83 | 84 | 85 | 86 | com.vivo.push.sdk 87 | vPush-server-sdk 88 | 1.0 89 | system 90 | ${project.basedir}/src/main/libs/vPush-server-sdk-1.0.jar 91 | 92 | 93 | 94 | 95 | 96 | com.getui.push 97 | restful-sdk 98 | 1.0.0.7 99 | 100 | 101 | 102 | 103 | org.apache.http 104 | httpclient 105 | 4.5 106 | system 107 | ${project.basedir}/src/main/libs/httpclient-4.5.jar 108 | 109 | 110 | 111 | 112 | org.apache.http 113 | httpcore 114 | 4.4.1 115 | system 116 | ${project.basedir}/src/main/libs/httpcore-4.4.1.jar 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | com.google.code.gson 125 | gson 126 | 2.8.9 127 | 128 | 129 | 130 | commons-io 131 | commons-io 132 | 2.7 133 | 134 | 135 | 136 | com.googlecode.json-simple 137 | json-simple 138 | 1.1.1 139 | 140 | 141 | 142 | org.slf4j 143 | slf4j-api 144 | 1.7.5 145 | 146 | 147 | 148 | org.slf4j 149 | slf4j-log4j12 150 | 1.7.5 151 | 152 | 153 | 154 | 155 | commons-httpclient 156 | commons-httpclient 157 | 3.1 158 | 159 | 160 | 161 | com.google.code.findbugs 162 | annotations 163 | 2.0.3 164 | provided 165 | 166 | 167 | 168 | com.fasterxml.jackson.core 169 | jackson-core 170 | 2.9.8 171 | 172 | 173 | com.fasterxml.jackson.core 174 | jackson-databind 175 | 2.9.10.8 176 | 177 | 178 | com.fasterxml.jackson.core 179 | jackson-annotations 180 | 2.9.8 181 | 182 | 183 | 184 | 185 | com.turo 186 | pushy 187 | 0.13.10 188 | 189 | 190 | 191 | com.turo 192 | pushy-micrometer-metrics-listener 193 | 0.13.10 194 | 195 | 196 | 197 | com.google.firebase 198 | firebase-admin 199 | 7.1.0 200 | 201 | 202 | 203 | com.google.guava 204 | guava 205 | 30.0-jre 206 | 207 | 208 | 209 | 210 | com.auth0 211 | java-jwt 212 | 3.8.1 213 | 214 | 215 | commons-codec 216 | commons-codec 217 | 1.16.0 218 | 219 | 220 | 221 | 222 | 223 | 224 | org.springframework.boot 225 | spring-boot-maven-plugin 226 | 227 | 228 | 229 | 230 | src/main/libs 231 | BOOT-INF/lib/ 232 | 233 | **/*.jar 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /push.md: -------------------------------------------------------------------------------- 1 | ## 接入推送流程 2 | 1. 申请厂商推送服务 3 | 2. 移动端配置 4 | 3. 配置和部署推送服务(push-server) 5 | 4. IM-Server 配置 6 | 5. 推送测试 7 | 6. 调试推送服务 8 | 7. 问题排查 9 | 10 | ### 一,申请厂商推送服务 11 | 目前支持小米、华为、vivo、oppo、魅族、苹果等推送,需要到各个厂商的开发者后台申请推送相关 key 12 | 13 | ### 二,移动端配置 14 | #### Android 端配置 15 | Android端,推送相关的代码,都在```push module``` 下面,入口是```PushService```,配置之后,如果能调用```ChatManager#setDeviceToken```,则表示配置成,下面是具体的配置 16 | 17 | 1. 修改```push/build.gradle```下推送相关配置信息,如下: 18 | ``` 19 | // 默认配置的 appid 和 appkey 不可以直接使用 20 | manifestPlaceholders = [ 21 | 22 | MI_APP_ID : "2882303761517722456", 23 | MI_APP_KEY : "5731772292456", 24 | 25 | HMS_APP_ID : "100221325", 26 | 27 | MEIZU_APP_ID : "113616", 28 | MEIZU_APP_KEY: "fcd886f51c144b45b87a67a28e2934d1", 29 | 30 | VIVO_APP_ID : "12918", 31 | VIVO_APP_KEY : "c42feb05-de6c-427d-af55-4f902d9e0a75", 32 | 33 | OPPO_APP_KEY : "16c6afe503b24259928e082ef01a6bf2", 34 | OPPO_APP_SECRET : "16c6afe503b24259928e082ef01a6bf2" 35 | ] 36 | ``` 37 | 2. 华为推送需要```chat/agconnect-services.json```文件,该文件是华为推送后台生成的 38 | 3. FCM推送需要替换```push/google-services.json```文件,该文件是 FCM 推送后台生成的 39 | 4. 如果一切正常,启动 App 之后,会打印下面这一行日志,**如果没有打印该日志,则说明配置错误,请查看日志,或者在```PushService```打断点调试**: 40 | 41 | ``` 42 | Log.d(TAG, "setDeviceToken" + token + " " + pushType);// 这是打印日志的代码!! 43 | ``` 44 | 5. 如果不需要某些推送类型,可以将其从```push module```删除,保留也不影响 45 | 6. 如果需要使用个推,请看```getui```分支 46 | 47 | #### iOS 端配置 48 | 请参考[ios-chat](https://github.com/wildfirechat/ios-chat)项目```appdelegate.m```文件中的关于推送注册的部分。当调用到```setDeviceToken```方法传入推送token即为客户端接入成功。 49 | 50 | ### 三,配置和部署推送服务 51 | 1. 修改配置 52 | 53 | 本推送服务有1个工程配置文件和7个推送配置文件,都在工程的```config```目录下,请根据实际情况配置服务的端口和各个推送服务配置,推送服务配置一定要和移动端对应上,别配置成不同的 app 去了。 54 | 55 | 如果有无法支持的推送类型,请修改客户端去掉不支持的类型(注意这里的配置文件要保留)。 56 | 57 | 2. 配置证书 58 | 59 | 苹果和谷歌推送需要证书,请把对应证书分别放到```apns```和```fcm```目录下,然后修改配置文件中的证书路径。 60 | 61 | 3. 编译 62 | ``` 63 | mvn package 64 | ``` 65 | 66 | 4. 运行 67 | 编译成功之后,在```target```目录找到```push-xxxx.jar```,然后把jar包、```config```目录、```apns```和```fcm```目录放到一起,然后执行下面命令: 68 | ``` 69 | nohup java -jar push-xxxx.jar 2>&1 & 70 | ``` 71 | 72 | ### 四,IM-Server 配置 73 | 修改IM服务的配置文件```wildfirechat.conf```,指向推送服务器的地址,修改完后需要重启 74 | ``` 75 | #********************************************************************* 76 | # Push server configuration 77 | #********************************************************************* 78 | ##安卓推送服务器地址 79 | push.android.server.address http://localhost:8085/android/push 80 | ##苹果推送服务器地址 81 | push.ios.server.address http://localhost:8085/ios/push 82 | ``` 83 | 84 | ### 五,推送测试 85 | 1. 确保双方都在线时,能互发消息 86 | 2. Android端,为了保证用户能正常收到消息,需要进行一些相关设置,产品上线之后,也需要引导用户进行相关设置,不设置会收到不到推送,具体设置方式不同手机不一样,请参考具体的手机设置,也可以参考[这儿](https://docs.rongcloud.cn/im/push/android/message_notification/): 87 | 1. 允许后台运行 88 | 2. 允许自启动 89 | 3. 允许后台弹出界面 90 | 4. 允许显示通知 91 | 3. 将其中一方杀进程,另外一方向其发送文本消息 92 | 4. 查看被杀进程一方,是否收到推送 93 | 94 | ### 六,调试推送服务 95 | 推送厂商SDK可能随时更新,接口也有可能由变更,推送服务和客户端推送SDK可能需要更新和调整。也有可能推送厂商开发后台配置错误,导致无法推送成功。出现这种情况后,请按照对应厂商的最新说明,调试推送服务。推送服务收到IM服务推送请求后,调用厂商SDK进行推送,这部分工作与野火IM无关了,请仔细阅读推送厂商的文档或者与联系推送厂商的技术支持。 96 | 97 | ### 七,问题排查 98 | 如果遇到问题请按照以下步骤排查: 99 | 1. 请确保上面所有步骤都正确完成之后,再开始问题排查 100 | 2. 确保程序是非启动状态,如果退回到桌面,应用还是激活的还会继续收消息,此时就不会走推送服务。应用在后台激活状态时应该走本地通知。 101 | 3. 确认客户端推送SDK是否正确的获取到token,是否调用了```setDeviceToken```,```token```和```type```是多少? 102 | 4. 上一步成功之后,```IM-Server```数据库的```t_user_session```表的```_token```和```_push_type```字段会被填上上一步设置的值。 103 | 5. 确认消息是否是自定义消息,如果是自定义消息,```push content```或者```push data```至少一个不为空才会推送。另外消息的[PersistFlag](https://docs.wildfirechat.cn/base_knowledge/message_content.html#消息类型)必须是存储或者存储计数属性的才会推送。 104 | 6. 确认目标客户端是否7日之内登录过,超过7天是不推送的。 105 | 7. 确认目标客户是否设置了全局静音或会话静音。 106 | 8. 如果有pc和web端登陆,确认是否设定了pc在线时手机静音。 107 | 9. 查看```IM-Server```日志,看是否有推送相关日志输出 108 | ``` 109 | LOG.info("Send push to {}, message from {}", deviceId, sender); // 这是打印日志的代码!! 110 | ``` 111 | 10. 确认推送服务是否收到了推送信息,如果收到,token和type是否和步骤1一致,推送内容是否和2一致? 112 | ``` 113 | // 目标用户是 Android 114 | LOG.info("Android push {}", new Gson().toJson(pushMessage)); // 这是打印日志的代码 115 | // 目标用户是 iOS 116 | LOG.info("iOS push {}", new Gson().toJson(pushMessage)); // 这是打印日志的代码 117 | ``` 118 | 11. 推送服务收到IM请求的推送信息,调用厂商SDK进行推送,检查代码确认是透传方式还是通知栏方式。 119 | 12. 如果推送服务使用的是通知栏方式,后面的工作就全是推送厂商的工作了,请按照推送厂商的官方文档进行调试。 120 | 13. 如果推送服务使用的是透传方式,请确认客户端对应推送SDK是否收到透传消息,如果没有收到透传消息,则问题出在推送通道上,请按照推送厂商的官方文档进行调试。 121 | 14. 如果推送服务使用的是透传方式,确认对应推送SDK收到了透传消息,请检查应用激活后是否初始化IM SDK并调用connect方法,及连接状态是否连接成功,是否收到新消息,是否弹出本地通知。 122 | 123 | ### 技术支持 124 | 按照文档一般情况下都能成功处理推送功能。实际上推送的功能并不复杂,只是涉及到太多的环节,每个环节又是由不同的研发或者公司来负责。请一定要理解整个推送的过程,知道推送过程中每一环节的功能,每个环节由谁来负责或者检查,只有真正的理解了推送的完整流程,才能找到对应的人来处理,才有可能高效地处理推送问题。 125 | 126 | 当确认是野火负责的环节时,可以来给野火提issue或者论坛发帖问。提问时请写清楚下面几个要求: 127 | 1. 请写出推送的完整流程。 128 | 2. 请写出推送的每个环节是那一方来负责,比如android端注册推送并调用setDeviceToken由你们android研发来负责。 129 | 3. 你认为那个环节出了问题,并给出证据。因为我们没有服务和客户端的任何信息,所以请务必给出全面详细的信息。 130 | 4. 上面问题排查中每一步的结果。 131 | 132 | **只有了解推送的流程和每个环节的功能才能高效地沟通,所以只有写清楚上面四条信息我们才能够提供技术支持。** 133 | 134 | ### 附录 135 | #### IM-Server 调用推送服务 HTTP 请求说明 136 | 使用POST方式,内容为JSON格式,参数如下 137 | 138 | | 参数 | 类型 | 必需 | 描述 | 139 | | ------ | ------ | --- | ------ | 140 | | sender | string | 是 | 发送者ID | 141 | | senderName | string | 是 | 发送者姓名 | 142 | | convType | int | 是 | 会话类型 | 143 | | target | string | 是 | 接收用户ID | 144 | | targetName | string | 是 | 接收用户名称 | 145 | | line | int | 否 | 会话线路,缺省为0 | 146 | | serverTime | long | 是 | 消息时间 | 147 | | pushMessageType | int | 是 | 0 普通消息;1 voip消息。在支持透传的系统上,voip消息用透传 | 148 | | pushType | int | 是 | 推送类型,android推送分为小米/华为/魅族等。ios分别为开发和发布。 | 149 | | pushContent | string | 是 | 消息推送内容 | 150 | | pushData | string | 否 | 消息推送数据 | 151 | | unReceivedMsg | int | 是 | 服务器端没有接收下来的消息数(只计算计数消息) | 152 | | mentionedType | int | 否 | 消息提醒类型,0,没提醒;1,提醒了当前用户;2,提醒了所有人 | 153 | | packageName | string | 否 | 应用包名 | 154 | | deviceToken | int | 否 | 设备token | 155 | | isHiddenDetail | bool | 否 | 是否要隐藏推送详情 | 156 | | language | string | 否 | 接收者的手机语言 | 157 | 158 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/PushApplication.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class PushApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(PushApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/PushController.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push; 2 | 3 | import cn.wildfirechat.push.android.AndroidPushService; 4 | import cn.wildfirechat.push.hm.HMPushService; 5 | import cn.wildfirechat.push.ios.IOSPushService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | public class PushController { 13 | 14 | @Autowired 15 | private AndroidPushService mAndroidPushService; 16 | 17 | @Autowired 18 | private IOSPushService mIOSPushService; 19 | 20 | @Autowired 21 | private HMPushService hmPushService; 22 | 23 | @PostMapping(value = "/android/push", produces = "application/json;charset=UTF-8") 24 | public Object androidPush(@RequestBody PushMessage pushMessage) { 25 | return mAndroidPushService.push(pushMessage); 26 | } 27 | 28 | @PostMapping(value = "/ios/push", produces = "application/json;charset=UTF-8") 29 | public Object iOSPush(@RequestBody PushMessage pushMessage) { 30 | return mIOSPushService.push(pushMessage); 31 | } 32 | 33 | @PostMapping(value = "/harmony/push", produces = "application/json;charset=UTF-8") 34 | public Object hmPush(@RequestBody PushMessage pushMessage) { 35 | return hmPushService.push(pushMessage); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/PushMessage.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push; 2 | 3 | 4 | public class PushMessage { 5 | public String sender; 6 | public String senderName; 7 | public String senderPortrait; 8 | public int convType; 9 | public String target; 10 | public String targetName; 11 | public String targetPortrait; 12 | public String userId; 13 | public int line; 14 | public int cntType; 15 | public long serverTime; 16 | //消息的类型,普通消息通知栏;voip要透传。 17 | public int pushMessageType; 18 | /** 19 | * 推送类型 20 | * Android 推送分为小米/华为/魅族等,参考{@link cn.wildfirechat.push.android.AndroidPushType} 21 | * iOS 分别为开发和发布,参考{@link cn.wildfirechat.push.ios.IOSPushType} 22 | */ 23 | public int pushType; 24 | public String pushContent; 25 | public String pushData; 26 | public int unReceivedMsg; 27 | public int mentionedType; 28 | public String packageName; 29 | public String deviceToken; 30 | public String voipDeviceToken; 31 | public boolean isHiddenDetail; 32 | public String language; 33 | public long messageId; 34 | public long callStartUid; 35 | //当消息被撤回/删除/更新时,这个值为true。 36 | public boolean republish; 37 | 38 | public String getSender() { 39 | return sender; 40 | } 41 | 42 | public void setSender(String sender) { 43 | this.sender = sender; 44 | } 45 | 46 | public String getSenderName() { 47 | return senderName; 48 | } 49 | 50 | public void setSenderName(String senderName) { 51 | this.senderName = senderName; 52 | } 53 | 54 | public int getConvType() { 55 | return convType; 56 | } 57 | 58 | public void setConvType(int convType) { 59 | this.convType = convType; 60 | } 61 | 62 | public String getTarget() { 63 | return target; 64 | } 65 | 66 | public void setTarget(String target) { 67 | this.target = target; 68 | } 69 | 70 | public String getTargetName() { 71 | return targetName; 72 | } 73 | 74 | public void setTargetName(String targetName) { 75 | this.targetName = targetName; 76 | } 77 | 78 | public int getLine() { 79 | return line; 80 | } 81 | 82 | public void setLine(int line) { 83 | this.line = line; 84 | } 85 | 86 | public int getCntType() { 87 | return cntType; 88 | } 89 | 90 | public void setCntType(int cntType) { 91 | this.cntType = cntType; 92 | } 93 | 94 | public long getServerTime() { 95 | return serverTime; 96 | } 97 | 98 | public void setServerTime(long serverTime) { 99 | this.serverTime = serverTime; 100 | } 101 | 102 | public int getPushMessageType() { 103 | return pushMessageType; 104 | } 105 | 106 | public void setPushMessageType(int pushMessageType) { 107 | this.pushMessageType = pushMessageType; 108 | } 109 | 110 | public int getPushType() { 111 | return pushType; 112 | } 113 | 114 | public void setPushType(int pushType) { 115 | this.pushType = pushType; 116 | } 117 | 118 | public String getPushContent() { 119 | return pushContent; 120 | } 121 | 122 | public void setPushContent(String pushContent) { 123 | this.pushContent = pushContent; 124 | } 125 | 126 | public String getPushData() { 127 | return pushData; 128 | } 129 | 130 | public void setPushData(String pushData) { 131 | this.pushData = pushData; 132 | } 133 | 134 | public int getUnReceivedMsg() { 135 | return unReceivedMsg; 136 | } 137 | 138 | public void setUnReceivedMsg(int unReceivedMsg) { 139 | this.unReceivedMsg = unReceivedMsg; 140 | } 141 | 142 | public int getMentionedType() { 143 | return mentionedType; 144 | } 145 | 146 | public void setMentionedType(int mentionedType) { 147 | this.mentionedType = mentionedType; 148 | } 149 | 150 | public String getPackageName() { 151 | return packageName; 152 | } 153 | 154 | public void setPackageName(String packageName) { 155 | this.packageName = packageName; 156 | } 157 | 158 | public String getDeviceToken() { 159 | return deviceToken; 160 | } 161 | 162 | public void setDeviceToken(String deviceToken) { 163 | this.deviceToken = deviceToken; 164 | } 165 | 166 | public String getVoipDeviceToken() { 167 | return voipDeviceToken; 168 | } 169 | 170 | public void setVoipDeviceToken(String voipDeviceToken) { 171 | this.voipDeviceToken = voipDeviceToken; 172 | } 173 | 174 | public boolean isHiddenDetail() { 175 | return isHiddenDetail; 176 | } 177 | 178 | public void setHiddenDetail(boolean hiddenDetail) { 179 | isHiddenDetail = hiddenDetail; 180 | } 181 | 182 | public String getLanguage() { 183 | return language; 184 | } 185 | 186 | public void setLanguage(String language) { 187 | this.language = language; 188 | } 189 | 190 | public boolean isRepublish() { 191 | return republish; 192 | } 193 | 194 | public void setRepublish(boolean republish) { 195 | this.republish = republish; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/PushMessageType.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push; 2 | 3 | public interface PushMessageType { 4 | int PUSH_MESSAGE_TYPE_NORMAL = 0; 5 | int PUSH_MESSAGE_TYPE_VOIP_INVITE = 1; 6 | int PUSH_MESSAGE_TYPE_VOIP_BYE = 2; 7 | int PUSH_MESSAGE_TYPE_FRIEND_REQUEST = 3; 8 | int PUSH_MESSAGE_TYPE_VOIP_ANSWER = 4; 9 | int PUSH_MESSAGE_TYPE_RECALLED = 5; 10 | int PUSH_MESSAGE_TYPE_DELETED = 6; 11 | int PUSH_MESSAGE_TYPE_SECRET_CHAT = 7; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/Utility.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push; 2 | 3 | import cn.wildfirechat.push.ios.IOSPushServiceImpl; 4 | import org.json.simple.JSONObject; 5 | import org.json.simple.parser.JSONParser; 6 | import org.json.simple.parser.ParseException; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.util.StringUtils; 10 | 11 | import java.util.Map; 12 | 13 | public class Utility { 14 | private static final Logger LOG = LoggerFactory.getLogger(Utility.class); 15 | 16 | public static String[] getPushTitleAndContent(PushMessage pushMessage) { 17 | String pushContent = pushMessage.getPushContent(); 18 | boolean hiddenDetail = pushMessage.isHiddenDetail; 19 | if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE) { 20 | pushContent = "通话邀请"; 21 | hiddenDetail = false; 22 | } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE) { 23 | pushContent = "通话结束"; 24 | hiddenDetail = false; 25 | } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_ANSWER) { 26 | pushContent = "已被其他端接听"; 27 | hiddenDetail = false; 28 | } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_RECALLED) { 29 | pushContent = "消息已被撤回"; 30 | hiddenDetail = false; 31 | pushMessage.pushData = null; 32 | } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_DELETED) { 33 | pushContent = "消息已被删除"; 34 | hiddenDetail = false; 35 | pushMessage.pushData = null; 36 | } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_SECRET_CHAT) { 37 | pushContent = "您收到一条密聊消息"; 38 | } else if(pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_NORMAL) { 39 | LOG.error("not support push message type:{}", pushMessage.pushMessageType); 40 | } 41 | 42 | String title; 43 | String body; 44 | 45 | //todo 这里需要加上语言的处理 46 | // if (pushMessage.language == "zh_CN") { 47 | // 48 | // } else if(pushMessage.language == "US_EN") { 49 | // 50 | // } 51 | if (pushMessage.convType == 1) { 52 | title = pushMessage.targetName; 53 | if (StringUtils.isEmpty(title)) { 54 | title = "群聊"; 55 | } 56 | 57 | if (StringUtils.isEmpty(pushMessage.senderName)) { 58 | body = pushContent; 59 | } else { 60 | body = pushMessage.senderName + ":" + pushContent; 61 | } 62 | 63 | if (hiddenDetail) { 64 | body = "你收到一条新消息"; //Todo 需要判断当前语言 65 | } 66 | 67 | if (pushMessage.mentionedType == 1) { 68 | if (StringUtils.isEmpty(pushMessage.senderName)) { 69 | body = "有人在群里@了你"; 70 | } else { 71 | body = pushMessage.senderName + "在群里@了你"; 72 | } 73 | } else if (pushMessage.mentionedType == 2) { 74 | if (StringUtils.isEmpty(pushMessage.senderName)) { 75 | body = "有人在群里@了大家"; 76 | } else { 77 | body = pushMessage.senderName + "在群里@了大家"; 78 | } 79 | } 80 | } else if(pushMessage.convType == 3) { 81 | title = pushMessage.targetName; 82 | if (hiddenDetail) { 83 | body = "你收到一条新消息"; //Todo 需要判断当前语言 84 | } else { 85 | body = pushContent; 86 | } 87 | } else { 88 | if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_FRIEND_REQUEST) { 89 | if (StringUtils.isEmpty(pushMessage.senderName)) { 90 | title = "好友请求"; 91 | } else { 92 | title = pushMessage.senderName + " 请求加您为好友"; 93 | } 94 | } else { 95 | if (StringUtils.isEmpty(pushMessage.senderName)) { 96 | title = "消息"; 97 | } else { 98 | title = pushMessage.senderName; 99 | } 100 | } 101 | 102 | if (hiddenDetail) { 103 | body = "你收到一条新消息"; //Todo 需要判断当前语言 104 | } else { 105 | body = pushContent; 106 | } 107 | } 108 | String[] result = new String[2]; 109 | result[0] = title; 110 | result[1] = body; 111 | return result; 112 | } 113 | 114 | public static boolean filterPush(PushMessage pushMessage) { 115 | if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE) { 116 | if(Utility.shouldCancelVoipByePush(pushMessage.sender, pushMessage.convType, pushMessage.userId, pushMessage.pushData)) { 117 | LOG.info("Voip bye push canceled"); 118 | return true; 119 | } 120 | } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_ANSWER) { 121 | if(Utility.shouldCancelVoipAnswerPush(pushMessage.sender, pushMessage.userId)) { 122 | LOG.info("Voip answer push canceled"); 123 | return true; 124 | } 125 | } 126 | return false; 127 | } 128 | /* 129 | 说明:1, 当回1对1通话时,发起方肯定在线,如果接收方不在线,发起方主动结束电话或者等待超时时需要推送对方。 130 | 2, 当是群组通话时,不在线的接收方可能回到结束推送,只有所有人都离开的结束原因才需要推送。 131 | */ 132 | private static boolean shouldCancelVoipByePush(String sender, int conversationType, String recvId, String pushData) { 133 | if(sender.equals(recvId)) { 134 | return true; 135 | } 136 | 137 | int reason = -1; 138 | if (!StringUtils.isEmpty(pushData)) { 139 | try { 140 | JSONObject object = (JSONObject) (new JSONParser().parse(pushData)); 141 | Object oEndReason = object.get("r"); 142 | if (oEndReason != null) { 143 | if (oEndReason instanceof Integer) { 144 | reason = (Integer) oEndReason; 145 | } else if (oEndReason instanceof Long) { 146 | long lr = (Long) oEndReason; 147 | reason = (int) lr; 148 | } 149 | } 150 | } catch (ParseException e) { 151 | e.printStackTrace(); 152 | } 153 | } else { 154 | return false; 155 | } 156 | 157 | LOG.info("End call reason is {}, convType is {}", reason, conversationType); 158 | if(reason > 0) { 159 | if(conversationType == 0) { 160 | //单人聊天。发起方在线,对方不在线。当对方超时或者当前方挂断,需要通知对方通话已经结束。 161 | if(reason == 3 || reason == 11) { 162 | return false; 163 | } 164 | } else if(conversationType == 1) { 165 | //群组聊天时,只有所有人离开时才需要推送未在线用户 166 | if(reason == 9) { 167 | return false; 168 | } 169 | } 170 | } 171 | return true; 172 | } 173 | 174 | //接听推送,只有自己发给自己用来停止掉其它端的振铃。 175 | private static boolean shouldCancelVoipAnswerPush(String sender, String recvId) { 176 | return !sender.equals(recvId); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/AndroidPushService.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | 5 | public interface AndroidPushService { 6 | Object push(PushMessage pushMessage); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/AndroidPushServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import cn.wildfirechat.push.Utility; 6 | import cn.wildfirechat.push.android.fcm.FCMPush; 7 | import cn.wildfirechat.push.android.getui.GetuiPush; 8 | import cn.wildfirechat.push.android.hms.HMSPush; 9 | import cn.wildfirechat.push.android.meizu.MeiZuPush; 10 | import cn.wildfirechat.push.android.oppo.OppoPush; 11 | import cn.wildfirechat.push.android.vivo.VivoPush; 12 | import cn.wildfirechat.push.android.xiaomi.XiaomiPush; 13 | import com.google.gson.Gson; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.stereotype.Service; 18 | 19 | import java.util.concurrent.ExecutorService; 20 | import java.util.concurrent.SynchronousQueue; 21 | import java.util.concurrent.ThreadPoolExecutor; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | @Service 25 | public class AndroidPushServiceImpl implements AndroidPushService { 26 | private static final Logger LOG = LoggerFactory.getLogger(AndroidPushServiceImpl.class); 27 | @Autowired 28 | private HMSPush hmsPush; 29 | 30 | @Autowired 31 | private MeiZuPush meiZuPush; 32 | 33 | @Autowired 34 | private XiaomiPush xiaomiPush; 35 | 36 | @Autowired 37 | private VivoPush vivoPush; 38 | 39 | @Autowired 40 | private OppoPush oppoPush; 41 | 42 | @Autowired 43 | private FCMPush fcmPush; 44 | 45 | @Autowired 46 | private GetuiPush getuiPush; 47 | 48 | private ExecutorService executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() * 100, 49 | 60L, TimeUnit.SECONDS, 50 | new SynchronousQueue()); 51 | 52 | @Override 53 | public Object push(PushMessage pushMessage) { 54 | LOG.info("Android push {}", new Gson().toJson(pushMessage)); 55 | if (Utility.filterPush(pushMessage)) { 56 | LOG.info("canceled"); 57 | return "Canceled"; 58 | } 59 | if(pushMessage.line == 1) { 60 | LOG.info("ignore moments messages"); 61 | return "Canceled"; 62 | } 63 | 64 | final long start = System.currentTimeMillis(); 65 | executorService.execute(() -> { 66 | long now = System.currentTimeMillis(); 67 | if (now - start > 15000) { 68 | LOG.error("等待太久,消息抛弃"); 69 | return; 70 | } 71 | 72 | switch (pushMessage.getPushType()) { 73 | case AndroidPushType.ANDROID_PUSH_TYPE_XIAOMI: 74 | xiaomiPush.push(pushMessage); 75 | break; 76 | case AndroidPushType.ANDROID_PUSH_TYPE_HUAWEI: 77 | hmsPush.push(pushMessage); 78 | break; 79 | case AndroidPushType.ANDROID_PUSH_TYPE_MEIZU: 80 | meiZuPush.push(pushMessage); 81 | break; 82 | case AndroidPushType.ANDROID_PUSH_TYPE_VIVO: 83 | vivoPush.push(pushMessage); 84 | break; 85 | case AndroidPushType.ANDROID_PUSH_TYPE_OPPO: 86 | oppoPush.push(pushMessage); 87 | break; 88 | case AndroidPushType.ANDROID_PUSH_TYPE_FCM: 89 | fcmPush.push(pushMessage); 90 | break; 91 | case AndroidPushType.ANDROID_PUSH_TYPE_GETUI: 92 | getuiPush.push(pushMessage, true); 93 | break; 94 | default: 95 | LOG.info("unknown push type"); 96 | break; 97 | } 98 | }); 99 | return "ok"; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/AndroidPushType.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android; 2 | 3 | public interface AndroidPushType { 4 | int ANDROID_PUSH_TYPE_XIAOMI = 1; 5 | int ANDROID_PUSH_TYPE_HUAWEI = 2; 6 | int ANDROID_PUSH_TYPE_MEIZU = 3; 7 | int ANDROID_PUSH_TYPE_VIVO = 4; 8 | int ANDROID_PUSH_TYPE_OPPO = 5; 9 | int ANDROID_PUSH_TYPE_FCM = 6; 10 | 11 | int ANDROID_PUSH_TYPE_GETUI = 7; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/fcm/FCMConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.fcm; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix="fcm") 9 | @PropertySource(value = "file:config/fcm.properties") 10 | public class FCMConfig { 11 | private String credentialsPath; 12 | 13 | public String getCredentialsPath() { 14 | return credentialsPath; 15 | } 16 | 17 | public void setCredentialsPath(String credentialsPath) { 18 | this.credentialsPath = credentialsPath; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/fcm/FCMPush.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.fcm; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import cn.wildfirechat.push.Utility; 6 | import com.google.auth.oauth2.GoogleCredentials; 7 | import com.google.firebase.FirebaseApp; 8 | import com.google.firebase.FirebaseOptions; 9 | import com.google.firebase.messaging.FirebaseMessaging; 10 | import com.google.firebase.messaging.FirebaseMessagingException; 11 | import com.google.firebase.messaging.Message; 12 | import com.google.firebase.messaging.Notification; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.stereotype.Component; 17 | import javax.annotation.PostConstruct; 18 | import java.io.FileInputStream; 19 | 20 | @Component 21 | public class FCMPush { 22 | private static final Logger LOG = LoggerFactory.getLogger(FCMPush.class); 23 | @Autowired 24 | private FCMConfig mConfig; 25 | 26 | @PostConstruct 27 | private void init() throws Exception { 28 | try { 29 | 30 | FileInputStream refreshToken = new FileInputStream(mConfig.getCredentialsPath()); 31 | FirebaseOptions options = FirebaseOptions.builder() 32 | .setCredentials(GoogleCredentials.fromStream(refreshToken)) 33 | .setDatabaseUrl("https://.firebaseio.com/") 34 | .build(); 35 | FirebaseApp.initializeApp(options); 36 | } catch (Exception e) { 37 | LOG.error("FCMPush init failed"); 38 | e.printStackTrace(); 39 | } 40 | } 41 | 42 | 43 | public void push(PushMessage pushMessage) { 44 | String[] arr = Utility.getPushTitleAndContent(pushMessage); 45 | String title = arr[0]; 46 | String body = arr[1]; 47 | 48 | Notification.Builder builder = Notification.builder().setTitle(title).setBody(body); 49 | Message message = Message.builder() 50 | .setNotification(builder.build()) 51 | .setToken(pushMessage.deviceToken) 52 | .build(); 53 | 54 | try { 55 | // Send a message to the device corresponding to the provided 56 | // registration token. 57 | String response = FirebaseMessaging.getInstance().send(message); 58 | // Response is a message ID string. 59 | System.out.println("Successfully sent message: " + response); 60 | } catch (FirebaseMessagingException e) { 61 | e.printStackTrace(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/getui/GetuiConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.getui; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix = "getui") 9 | @PropertySource(value = "file:config/getui.properties") 10 | public class GetuiConfig { 11 | private String appId; 12 | private String appKey; 13 | private String masterSecret; 14 | private String domain; 15 | 16 | public String getAppId() { 17 | return appId; 18 | } 19 | 20 | public void setAppId(String appId) { 21 | this.appId = appId; 22 | } 23 | 24 | public String getAppKey() { 25 | return appKey; 26 | } 27 | 28 | public void setAppKey(String appKey) { 29 | this.appKey = appKey; 30 | } 31 | 32 | public String getMasterSecret() { 33 | return masterSecret; 34 | } 35 | 36 | public void setMasterSecret(String masterSecret) { 37 | this.masterSecret = masterSecret; 38 | } 39 | 40 | public String getDomain() { 41 | return domain; 42 | } 43 | 44 | public void setDomain(String domain) { 45 | this.domain = domain; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/getui/GetuiPush.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.getui; 2 | 3 | 4 | import cn.wildfirechat.push.PushMessage; 5 | import cn.wildfirechat.push.PushMessageType; 6 | import cn.wildfirechat.push.Utility; 7 | import cn.wildfirechat.push.android.xiaomi.XiaomiConfig; 8 | import com.getui.push.v2.sdk.ApiHelper; 9 | import com.getui.push.v2.sdk.GtApiConfiguration; 10 | import com.getui.push.v2.sdk.api.PushApi; 11 | import com.getui.push.v2.sdk.common.ApiResult; 12 | import com.getui.push.v2.sdk.dto.req.Audience; 13 | import com.getui.push.v2.sdk.dto.req.message.PushChannel; 14 | import com.getui.push.v2.sdk.dto.req.message.PushDTO; 15 | import com.getui.push.v2.sdk.dto.req.message.android.AndroidDTO; 16 | import com.getui.push.v2.sdk.dto.req.message.android.GTNotification; 17 | import com.getui.push.v2.sdk.dto.req.message.android.ThirdNotification; 18 | import com.getui.push.v2.sdk.dto.req.message.android.Ups; 19 | import com.getui.push.v2.sdk.dto.req.message.ios.Alert; 20 | import com.getui.push.v2.sdk.dto.req.message.ios.Aps; 21 | import com.getui.push.v2.sdk.dto.req.message.ios.IosDTO; 22 | import com.google.gson.Gson; 23 | import com.meizu.push.sdk.server.IFlymePush; 24 | import com.xiaomi.xmpush.server.Constants; 25 | import com.xiaomi.xmpush.server.Message; 26 | import com.xiaomi.xmpush.server.Result; 27 | import com.xiaomi.xmpush.server.Sender; 28 | import org.json.simple.parser.ParseException; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | import org.springframework.beans.factory.annotation.Autowired; 32 | import org.springframework.stereotype.Component; 33 | import org.springframework.util.StringUtils; 34 | 35 | import javax.annotation.PostConstruct; 36 | import java.io.IOException; 37 | import java.util.Map; 38 | 39 | import static com.xiaomi.xmpush.server.Message.NOTIFY_TYPE_ALL; 40 | 41 | @Component 42 | public class GetuiPush { 43 | private static final Logger LOG = LoggerFactory.getLogger(GetuiPush.class); 44 | @Autowired 45 | private GetuiConfig mConfig; 46 | 47 | private PushApi pushApi; 48 | 49 | @PostConstruct 50 | public void init() { 51 | try{ 52 | // 设置httpClient最大连接数,当并发较大时建议调大此参数。或者启动参数加上 -Dhttp.maxConnections=200 53 | System.setProperty("http.maxConnections", "200"); 54 | GtApiConfiguration apiConfiguration = new GtApiConfiguration(); 55 | //填写应用配置 56 | if(!StringUtils.isEmpty(mConfig.getAppId())) { 57 | apiConfiguration.setAppId(mConfig.getAppId()); 58 | apiConfiguration.setAppKey(mConfig.getAppKey()); 59 | apiConfiguration.setMasterSecret(mConfig.getMasterSecret()); 60 | // 接口调用前缀,请查看文档: 接口调用规范 -> 接口前缀, 可不填写appId 61 | apiConfiguration.setDomain("https://restapi.getui.com/v2/"); 62 | // apiConfiguration.setDomain(mConfig.getDomain()); 63 | // 实例化ApiHelper对象,用于创建接口对象 64 | ApiHelper apiHelper = ApiHelper.build(apiConfiguration); 65 | // 创建对象,建议复用。目前有PushApi、StatisticApi、UserApi 66 | this.pushApi = apiHelper.creatApi(PushApi.class); 67 | } 68 | } catch (Exception e) { 69 | LOG.error("GetuiPush init failed"); 70 | e.printStackTrace(); 71 | } 72 | } 73 | 74 | public void push(PushMessage pushMessage, boolean isAndroid) { 75 | String[] arr = Utility.getPushTitleAndContent(pushMessage); 76 | String title = arr[0]; 77 | String body = arr[1]; 78 | 79 | //根据cid进行单推 80 | PushDTO pushDTO = new PushDTO(); 81 | // 设置推送参数 82 | pushDTO.setRequestId(System.currentTimeMillis() + ""); 83 | /**** 设置个推通道参数 *****/ 84 | com.getui.push.v2.sdk.dto.req.message.PushMessage pm = new com.getui.push.v2.sdk.dto.req.message.PushMessage(); 85 | pushDTO.setPushMessage(pm); 86 | GTNotification notification = new GTNotification(); 87 | pm.setNotification(notification); 88 | notification.setTitle(title); 89 | notification.setBody(body); 90 | notification.setClickType("startapp"); 91 | // notification.setUrl("https://www.getui.com"); 92 | /**** 设置个推通道参数,更多参数请查看文档或对象源码 *****/ 93 | 94 | /**** 设置厂商相关参数 ****/ 95 | PushChannel pushChannel = new PushChannel(); 96 | pushDTO.setPushChannel(pushChannel); 97 | /*配置安卓厂商参数*/ 98 | AndroidDTO androidDTO = new AndroidDTO(); 99 | pushChannel.setAndroid(androidDTO); 100 | Ups ups = new Ups(); 101 | androidDTO.setUps(ups); 102 | ThirdNotification thirdNotification = new ThirdNotification(); 103 | ups.setNotification(thirdNotification); 104 | thirdNotification.setTitle(title); 105 | thirdNotification.setBody(body); 106 | thirdNotification.setClickType("startapp"); 107 | // thirdNotification.setUrl("https://www.getui.com"); 108 | // 两条消息的notify_id相同,新的消息会覆盖老的消息,取值范围:0-2147483647 109 | // thirdNotification.setNotifyId("11177"); 110 | /*配置安卓厂商参数结束,更多参数请查看文档或对象源码*/ 111 | 112 | /*设置ios厂商参数*/ 113 | IosDTO iosDTO = new IosDTO(); 114 | pushChannel.setIos(iosDTO); 115 | if (pushMessage.pushMessageType == 1 || pushMessage.pushMessageType == 2) { 116 | iosDTO.setType("voip"); 117 | } 118 | // 相同的collapseId会覆盖之前的消息 119 | iosDTO.setApnsCollapseId("" + pushMessage.messageId); 120 | Aps aps = new Aps(); 121 | iosDTO.setAps(aps); 122 | Alert alert = new Alert(); 123 | aps.setAlert(alert); 124 | alert.setTitle(title); 125 | alert.setBody(body); 126 | /*设置ios厂商参数结束,更多参数请查看文档或对象源码*/ 127 | 128 | /*设置接收人信息*/ 129 | Audience audience = new Audience(); 130 | pushDTO.setAudience(audience); 131 | audience.addCid(pushMessage.getDeviceToken()); 132 | /*设置接收人信息结束*/ 133 | /**** 设置厂商相关参数,更多参数请查看文档或对象源码 ****/ 134 | 135 | // 进行cid单推 136 | ApiResult>> apiResult = pushApi.pushToSingleByCid(pushDTO); 137 | if (apiResult.isSuccess()) { 138 | // success 139 | System.out.println(apiResult.getData()); 140 | } else { 141 | // failed 142 | System.out.println("code:" + apiResult.getCode() + ", msg: " + apiResult.getMsg()); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/hms/HMSConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.hms; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix="hms") 9 | @PropertySource(value = "file:config/hms.properties") 10 | public class HMSConfig { 11 | private String appSecret; 12 | private String appId; 13 | 14 | public String getAppSecret() { 15 | return appSecret; 16 | } 17 | 18 | public void setAppSecret(String appSecret) { 19 | this.appSecret = appSecret; 20 | } 21 | 22 | public String getAppId() { 23 | return appId; 24 | } 25 | 26 | public void setAppId(String appId) { 27 | this.appId = appId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/hms/HMSPush.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.hms; 2 | 3 | 4 | import cn.wildfirechat.push.PushMessage; 5 | import cn.wildfirechat.push.PushMessageType; 6 | import com.alibaba.fastjson.JSONArray; 7 | import com.alibaba.fastjson.JSONObject; 8 | import com.google.gson.Gson; 9 | import org.apache.commons.io.IOUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.OutputStream; 18 | import java.net.HttpURLConnection; 19 | import java.net.URL; 20 | import java.net.URLEncoder; 21 | import java.text.MessageFormat; 22 | import java.util.List; 23 | 24 | @Component 25 | public class HMSPush { 26 | private static final Logger LOG = LoggerFactory.getLogger(HMSPush.class); 27 | private static final String tokenUrl = "https://login.vmall.com/oauth2/token"; //获取认证Token的URL 28 | private static final String apiUrl = "https://api.push.hicloud.com/pushsend.do"; //应用级消息下发API 29 | private String accessToken;//下发通知消息的认证Token 30 | private long tokenExpiredTime; //accessToken的过期时间 31 | 32 | @Autowired 33 | private HMSConfig mConfig; 34 | 35 | //获取下发通知消息的认证Token 36 | private void refreshToken() throws IOException { 37 | LOG.info("hms refresh token"); 38 | String msgBody = MessageFormat.format( 39 | "grant_type=client_credentials&client_secret={0}&client_id={1}", 40 | URLEncoder.encode(mConfig.getAppSecret(), "UTF-8"), mConfig.getAppId()); 41 | String response = httpPost(tokenUrl, msgBody, 5000, 5000); 42 | JSONObject obj = JSONObject.parseObject(response); 43 | accessToken = obj.getString("access_token"); 44 | tokenExpiredTime = System.currentTimeMillis() + obj.getLong("expires_in") - 5*60*1000; 45 | LOG.info("hms refresh token with result {}", response); 46 | } 47 | 48 | //发送Push消息 49 | public void push(PushMessage pushMessage) { 50 | if (tokenExpiredTime <= System.currentTimeMillis()) { 51 | try { 52 | refreshToken(); 53 | } catch (IOException e) { 54 | e.printStackTrace(); 55 | } 56 | } 57 | /*PushManager.requestToken为客户端申请token的方法,可以调用多次以防止申请token失败*/ 58 | /*PushToken不支持手动编写,需使用客户端的onToken方法获取*/ 59 | JSONArray deviceTokens = new JSONArray();//目标设备Token 60 | deviceTokens.add(pushMessage.getDeviceToken()); 61 | 62 | 63 | JSONObject msg = new JSONObject(); 64 | msg.put("type", 1);//3: 通知栏消息,异步透传消息请根据接口文档设置 65 | String token = pushMessage.getDeviceToken(); 66 | pushMessage.deviceToken = null; 67 | msg.put("body", new Gson().toJson(pushMessage));//通知栏消息body内容 68 | 69 | JSONObject hps = new JSONObject();//华为PUSH消息总结构体 70 | hps.put("msg", msg); 71 | 72 | JSONObject payload = new JSONObject(); 73 | payload.put("hps", hps); 74 | 75 | LOG.info("send push to HMS {}", payload); 76 | 77 | try { 78 | String postBody = MessageFormat.format( 79 | "access_token={0}&nsp_svc={1}&nsp_ts={2}&device_token_list={3}&payload={4}", 80 | URLEncoder.encode(accessToken,"UTF-8"), 81 | URLEncoder.encode("openpush.message.api.send","UTF-8"), 82 | URLEncoder.encode(String.valueOf(System.currentTimeMillis() / 1000),"UTF-8"), 83 | URLEncoder.encode(deviceTokens.toString(),"UTF-8"), 84 | URLEncoder.encode(payload.toString(),"UTF-8")); 85 | 86 | String postUrl = apiUrl + "?nsp_ctx=" + URLEncoder.encode("{\"ver\":\"1\", \"appId\":\"" + mConfig.getAppId() + "\"}", "UTF-8"); 87 | String response = httpPost(postUrl, postBody, 5000, 5000); 88 | LOG.info("Push to {} response {}", token, response); 89 | } catch (IOException e) { 90 | e.printStackTrace(); 91 | LOG.info("Push to {} with exception", token, e); 92 | } 93 | } 94 | 95 | public String httpPost(String httpUrl, String data, int connectTimeout, int readTimeout) throws IOException { 96 | OutputStream outPut = null; 97 | HttpURLConnection urlConnection = null; 98 | InputStream in = null; 99 | 100 | try { 101 | URL url = new URL(httpUrl); 102 | urlConnection = (HttpURLConnection)url.openConnection(); 103 | urlConnection.setRequestMethod("POST"); 104 | urlConnection.setDoOutput(true); 105 | urlConnection.setDoInput(true); 106 | urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); 107 | urlConnection.setConnectTimeout(connectTimeout); 108 | urlConnection.setReadTimeout(readTimeout); 109 | urlConnection.connect(); 110 | 111 | // POST data 112 | outPut = urlConnection.getOutputStream(); 113 | outPut.write(data.getBytes("UTF-8")); 114 | outPut.flush(); 115 | 116 | // read response 117 | if (urlConnection.getResponseCode() < 400) { 118 | in = urlConnection.getInputStream(); 119 | } else { 120 | in = urlConnection.getErrorStream(); 121 | } 122 | 123 | List lines = IOUtils.readLines(in, urlConnection.getContentEncoding()); 124 | StringBuffer strBuf = new StringBuffer(); 125 | for (String line : lines) { 126 | strBuf.append(line); 127 | } 128 | LOG.info(strBuf.toString()); 129 | return strBuf.toString(); 130 | } 131 | finally { 132 | IOUtils.closeQuietly(outPut); 133 | IOUtils.closeQuietly(in); 134 | if (urlConnection != null) { 135 | urlConnection.disconnect(); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/meizu/MeiZuConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.meizu; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix="meizu") 9 | @PropertySource(value = "file:config/meizu.properties") 10 | public class MeiZuConfig { 11 | private String appSecret; 12 | private long appId; 13 | 14 | public String getAppSecret() { 15 | return appSecret; 16 | } 17 | 18 | public void setAppSecret(String appSecret) { 19 | this.appSecret = appSecret; 20 | } 21 | 22 | public long getAppId() { 23 | return appId; 24 | } 25 | 26 | public void setAppId(long appId) { 27 | this.appId = appId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/meizu/MeiZuPush.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.meizu; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import com.meizu.push.sdk.server.IFlymePush; 6 | import com.meizu.push.sdk.server.constant.ResultPack; 7 | import com.meizu.push.sdk.server.model.push.PushResult; 8 | import com.meizu.push.sdk.server.model.push.VarnishedMessage; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.util.StringUtils; 14 | 15 | import javax.annotation.PostConstruct; 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | @Component 22 | public class MeiZuPush { 23 | private static final Logger LOG = LoggerFactory.getLogger(MeiZuPush.class); 24 | private IFlymePush flymePush; 25 | 26 | @PostConstruct 27 | public void init() { 28 | try { 29 | //初始化推送sdk 30 | this.flymePush = new IFlymePush(mConfig.getAppSecret()); 31 | } catch (Exception e) { 32 | LOG.error("MeiZuPush init failed"); 33 | e.printStackTrace(); 34 | } 35 | } 36 | 37 | @Autowired 38 | private MeiZuConfig mConfig; 39 | 40 | public void push(PushMessage pushMessage) { 41 | if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_RECALLED || pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_DELETED) { 42 | //Todo not implement 43 | //撤回或者删除消息,需要更新远程通知,暂未实现 44 | return; 45 | } 46 | 47 | //组装透传消息 48 | String title; 49 | if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_FRIEND_REQUEST) { 50 | if (StringUtils.isEmpty(pushMessage.senderName)) { 51 | title = "好友请求"; 52 | } else { 53 | title = pushMessage.senderName + " 请求加您为好友"; 54 | } 55 | } else { 56 | if (StringUtils.isEmpty(pushMessage.senderName)) { 57 | title = "消息"; 58 | } else { 59 | title = pushMessage.senderName; 60 | } 61 | } 62 | 63 | if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_SECRET_CHAT) { 64 | pushMessage.pushContent = "您收到一条密聊消息"; 65 | } 66 | 67 | VarnishedMessage message = new VarnishedMessage.Builder() 68 | .appId(mConfig.getAppId()) 69 | .title(title) 70 | .content(pushMessage.pushContent) 71 | .validTime(1) 72 | .build(); 73 | 74 | //目标用户 75 | List pushIds = new ArrayList(); 76 | pushIds.add(pushMessage.getDeviceToken()); 77 | 78 | try { 79 | // 1 调用推送服务 80 | ResultPack result = flymePush.pushMessage(message, pushIds); 81 | if (result.isSucceed()) { 82 | // 2 调用推送服务成功 (其中map为设备的具体推送结果,一般业务针对超速的code类型做处理) 83 | PushResult pushResult = result.value(); 84 | String msgId = pushResult.getMsgId();//推送消息ID,用于推送流程明细排查 85 | Map> targetResultMap = pushResult.getRespTarget();//推送结果,全部推送成功,则map为empty 86 | LOG.info("push result:" + pushResult); 87 | if (targetResultMap != null && !targetResultMap.isEmpty()) { 88 | System.err.println("push fail token:" + targetResultMap); 89 | } 90 | } else { 91 | // 调用推送接口服务异常 eg: appId、appKey非法、推送消息非法..... 92 | // result.code(); //服务异常码 93 | // result.comment();//服务异常描述 94 | LOG.info(String.format("pushMessage error code:%s comment:%s", result.code(), result.comment())); 95 | } 96 | } catch (IOException e) { 97 | e.printStackTrace(); 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/oppo/OppoConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.oppo; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix = "oppo") 9 | @PropertySource(value = "file:config/oppo.properties") 10 | public class OppoConfig { 11 | private String appSecret; 12 | private String appKey; 13 | 14 | public String getAppSecret() { 15 | return appSecret; 16 | } 17 | 18 | public void setAppSecret(String appSecret) { 19 | this.appSecret = appSecret; 20 | } 21 | 22 | public String getAppKey() { 23 | return appKey; 24 | } 25 | 26 | public void setAppKey(String appKey) { 27 | this.appKey = appKey; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/oppo/OppoPush.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.oppo; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import cn.wildfirechat.push.Utility; 6 | import com.oppo.push.server.Notification; 7 | import com.oppo.push.server.Result; 8 | import com.oppo.push.server.Sender; 9 | import com.oppo.push.server.Target; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.util.StringUtils; 15 | 16 | import javax.annotation.PostConstruct; 17 | 18 | @Component 19 | public class OppoPush { 20 | private static final Logger LOG = LoggerFactory.getLogger(OppoPush.class); 21 | private static final int OPPO_PUSH_MAX_CONTENT = 200; 22 | 23 | @Autowired 24 | OppoConfig mConfig; 25 | 26 | private Sender mSender; 27 | 28 | @PostConstruct 29 | private void init() { 30 | try { 31 | mSender = new Sender(mConfig.getAppKey(), mConfig.getAppSecret()); 32 | } catch (Exception e) { 33 | LOG.error("OppoPush init failed"); 34 | e.printStackTrace(); 35 | } 36 | } 37 | 38 | 39 | 40 | public void push(PushMessage pushMessage) { 41 | if (mSender == null) { 42 | LOG.error("Oppo push message can't sent, because not initial correctly"); 43 | } 44 | 45 | Result result = null; 46 | try { 47 | Notification notification = getNotification(pushMessage); //创建通知栏消息体 48 | 49 | Target target = Target.build(pushMessage.deviceToken); //创建发送对象 50 | 51 | result = mSender.unicastNotification(notification, target); //发送单推消息 52 | 53 | result.getStatusCode(); // 获取http请求状态码 54 | 55 | result.getReturnCode(); // 获取平台返回码 56 | 57 | result.getMessageId(); // 获取平台返回的messageId 58 | } catch (Exception e) { 59 | e.printStackTrace(); 60 | LOG.error("sendSingle error " + e.getMessage()); 61 | } 62 | if (result != null) { 63 | LOG.info("Server response: MessageId: " + result.getMessageId() 64 | + " ErrorCode: " + result.getReturnCode() 65 | + " Reason: " + result.getReason()); 66 | } 67 | } 68 | 69 | private Notification getNotification(PushMessage pushMessage) { 70 | if (pushMessage.isHiddenDetail) { 71 | pushMessage.pushContent = "您收到一条新消息"; 72 | } 73 | Notification notification = new Notification(); 74 | 75 | 76 | /** 77 | * 以下参数必填项 78 | */ 79 | String[] arr = Utility.getPushTitleAndContent(pushMessage); 80 | String title = arr[0]; 81 | String body = arr[1]; 82 | if(body != null && body.length() > OPPO_PUSH_MAX_CONTENT) { 83 | body = body.substring(0, OPPO_PUSH_MAX_CONTENT-3); 84 | body += "..."; 85 | } 86 | 87 | notification.setTitle(title); 88 | notification.setContent(body); 89 | 90 | /** 91 | * 以下参数非必填项, 如果需要使用可以参考OPPO push服务端api文档进行设置 92 | */ 93 | //通知栏样式 1. 标准样式 2. 长文本样式 3. 大图样式 【非必填,默认1-标准样式】 94 | notification.setStyle(1); 95 | 96 | // App开发者自定义消息Id,OPPO推送平台根据此ID做去重处理,对于广播推送相同appMessageId只会保存一次,对于单推相同appMessageId只会推送一次 97 | //notification.setAppMessageId(UUID.randomUUID().toString()); 98 | 99 | // 应用接收消息到达回执的回调URL,字数限制200以内,中英文均以一个计算 100 | //notification.setCallBackUrl("http://www.test.com"); 101 | 102 | // App开发者自定义回执参数,字数限制50以内,中英文均以一个计算 103 | //notification.setCallBackParameter(""); 104 | 105 | // 点击动作类型0,启动应用;1,打开应用内页(activity的intent action);2,打开网页;4,打开应用内页(activity);【非必填,默认值为0】;5,Intent scheme URL 106 | //notification.setClickActionType(4); 107 | 108 | // 应用内页地址【click_action_type为1或4时必填,长度500】 109 | //notification.setClickActionActivity("com.coloros.push.demo.component.InternalActivity"); 110 | 111 | // 网页地址【click_action_type为2必填,长度500】 112 | //notification.setClickActionUrl("http://www.test.com"); 113 | 114 | // 动作参数,打开应用内页或网页时传递给应用或网页【JSON格式,非必填】,字符数不能超过4K,示例:{"key1":"value1","key2":"value2"} 115 | //notification.setActionParameters("{\"key1\":\"value1\",\"key2\":\"value2\"}"); 116 | 117 | // 展示类型 (0, “即时”),(1, “定时”) 118 | notification.setShowTimeType(0); 119 | 120 | // 定时展示开始时间(根据time_zone转换成当地时间),时间的毫秒数 121 | //notification.setShowStartTime(System.currentTimeMillis() + 1000 * 60 * 3); 122 | 123 | // 定时展示结束时间(根据time_zone转换成当地时间),时间的毫秒数 124 | //notification.setShowEndTime(System.currentTimeMillis() + 1000 * 60 * 5); 125 | 126 | // 是否进离线消息,【非必填,默认为True】 127 | //notification.setOffLine(true); 128 | 129 | // 离线消息的存活时间(time_to_live) (单位:秒), 【off_line值为true时,必填,最长3天】 130 | if (pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_NORMAL) { 131 | notification.setOffLineTtl(60); // 单位秒 132 | } else { 133 | notification.setOffLineTtl(10 * 60); 134 | } 135 | 136 | // 时区,默认值:(GMT+08:00)北京,香港,新加坡 137 | //notification.setTimeZone("GMT+08:00"); 138 | 139 | // 0:不限联网方式, 1:仅wifi推送 140 | notification.setNetworkType(0); 141 | 142 | return notification; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/vivo/VivoConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.vivo; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix = "vivo") 9 | @PropertySource(value = "file:config/vivo.properties") 10 | public class VivoConfig { 11 | private String appSecret; 12 | private int appId; 13 | private String appKey; 14 | 15 | public String getAppSecret() { 16 | return appSecret; 17 | } 18 | 19 | public void setAppSecret(String appSecret) { 20 | this.appSecret = appSecret; 21 | } 22 | 23 | public int getAppId() { 24 | return appId; 25 | } 26 | 27 | public void setAppId(int appId) { 28 | this.appId = appId; 29 | } 30 | 31 | public String getAppKey() { 32 | return appKey; 33 | } 34 | 35 | public void setAppKey(String appKey) { 36 | this.appKey = appKey; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/vivo/VivoPush.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.vivo; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import cn.wildfirechat.push.Utility; 6 | import com.vivo.push.sdk.notofication.Message; 7 | import com.vivo.push.sdk.notofication.Result; 8 | import com.vivo.push.sdk.server.Sender; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.util.StringUtils; 14 | 15 | import javax.annotation.PostConstruct; 16 | import java.io.IOException; 17 | 18 | @Component 19 | public class VivoPush { 20 | private static final Logger LOG = LoggerFactory.getLogger(VivoPush.class); 21 | private long tokenExpiredTime; 22 | 23 | @Autowired 24 | VivoConfig mConfig; 25 | 26 | private String authToken; 27 | 28 | private void refreshToken() { 29 | Sender sender = null;//注册登录开发平台网站获取到的appSecret
 30 | try { 31 | sender = new Sender(mConfig.getAppSecret()); 32 | Result result = sender.getToken(mConfig.getAppId(), mConfig.getAppKey());//注册登录开发平台网站获取到的appId和appKey
 33 | authToken = result.getAuthToken(); 34 | tokenExpiredTime = System.currentTimeMillis() + 12 * 60 * 60 * 1000; 35 | } catch (Exception e) { 36 | e.printStackTrace(); 37 | LOG.error("getToken error" + e.getMessage()); 38 | } 39 | } 40 | 41 | public void push(PushMessage pushMessage) { 42 | if (tokenExpiredTime <= System.currentTimeMillis()) { 43 | refreshToken(); 44 | } 45 | 46 | Result resultMessage = null; 47 | try { 48 | String[] arr = Utility.getPushTitleAndContent(pushMessage); 49 | String title = arr[0]; 50 | String body = arr[1]; 51 | 52 | Sender senderMessage = new Sender(mConfig.getAppSecret(), authToken); 53 | Message.Builder builder = new Message.Builder() 54 | .regId(pushMessage.getDeviceToken())//该测试手机设备订阅推送后生成的regId
 55 | .notifyType(3) 56 | .title(title) 57 | .content(body) 58 | .timeToLive(1000) 59 | .skipType(1) 60 | .networkType(-1) 61 | .requestId(System.currentTimeMillis() + ""); 62 | if (pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_NORMAL) { 63 | builder.timeToLive(60); // 单位秒 64 | } else { 65 | builder.timeToLive(10 * 60); 66 | } 67 | resultMessage = senderMessage.sendSingle(builder.build()); 68 | } catch (Exception e) { 69 | e.printStackTrace(); 70 | LOG.error("sendSingle error " + e.getMessage()); 71 | } 72 | if (resultMessage != null) { 73 | 74 | LOG.info("Server response: MessageId: " + resultMessage.getTaskId() 75 | + " ErrorCode: " + resultMessage.getResult() 76 | + " Reason: " + resultMessage.getDesc()); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.xiaomi; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix="xiaomi") 9 | @PropertySource(value = "file:config/xiaomi.properties") 10 | public class XiaomiConfig { 11 | private String appSecret; 12 | private String channelId; 13 | 14 | public String getAppSecret() { 15 | return appSecret; 16 | } 17 | 18 | public void setAppSecret(String appSecret) { 19 | this.appSecret = appSecret; 20 | } 21 | 22 | public String getChannelId() { 23 | return channelId; 24 | } 25 | 26 | public void setChannelId(String channelId) { 27 | this.channelId = channelId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiPush.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.android.xiaomi; 2 | 3 | 4 | import cn.wildfirechat.push.PushMessage; 5 | import cn.wildfirechat.push.PushMessageType; 6 | import cn.wildfirechat.push.Utility; 7 | import com.google.gson.Gson; 8 | import com.xiaomi.xmpush.server.Constants; 9 | import com.xiaomi.xmpush.server.Message; 10 | import com.xiaomi.xmpush.server.Result; 11 | import com.xiaomi.xmpush.server.Sender; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Component; 16 | import org.json.simple.parser.ParseException; 17 | 18 | 19 | import java.io.IOException; 20 | 21 | import static com.xiaomi.xmpush.server.Message.NOTIFY_TYPE_ALL; 22 | 23 | @Component 24 | public class XiaomiPush { 25 | private static final Logger LOG = LoggerFactory.getLogger(XiaomiPush.class); 26 | @Autowired 27 | private XiaomiConfig mConfig; 28 | 29 | 30 | public void push(PushMessage pushMessage) { 31 | Constants.useOfficial(); 32 | Sender sender = new Sender(mConfig.getAppSecret()); 33 | 34 | if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_SECRET_CHAT) { 35 | pushMessage.pushContent = "您收到一条密聊消息"; 36 | } 37 | 38 | Message message; 39 | String token = pushMessage.getDeviceToken(); 40 | pushMessage.deviceToken = null; 41 | if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE || pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE || pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_ANSWER) { 42 | //voip 43 | long timeToLive = 60 * 1000; // 1 min 44 | message = new Message.Builder() 45 | .payload(new Gson().toJson(pushMessage)) 46 | .restrictedPackageName(pushMessage.getPackageName()) 47 | .passThrough(1) //透传 48 | .timeToLive(timeToLive) 49 | .enableFlowControl(false) 50 | .extra("channel_id", mConfig.getChannelId()) 51 | .build(); 52 | } else { //normal or friend 53 | String[] arr = Utility.getPushTitleAndContent(pushMessage); 54 | String title = arr[0]; 55 | String body = arr[1]; 56 | 57 | long timeToLive = 600 * 1000;//10 min 58 | message = new Message.Builder() 59 | .payload(new Gson().toJson(pushMessage)) 60 | .title(title) 61 | .description(body) 62 | .notifyType(NOTIFY_TYPE_ALL) 63 | .restrictedPackageName(pushMessage.getPackageName()) 64 | .passThrough(0) 65 | .timeToLive(timeToLive) 66 | .enableFlowControl(true) 67 | .extra("channel_id", mConfig.getChannelId()) 68 | .build(); 69 | } 70 | 71 | Result result = null; 72 | try { 73 | result = sender.send(message, token, 3); 74 | } catch (IOException e) { 75 | e.printStackTrace(); 76 | } catch (ParseException e) { 77 | e.printStackTrace(); 78 | } 79 | 80 | LOG.info("Server response: MessageId: " + result.getMessageId() 81 | + " ErrorCode: " + result.getErrorCode().toString() 82 | + " Reason: " + result.getReason()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/HMConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix = "hm") 9 | @PropertySource(value = "file:config/hm.properties") 10 | public class HMConfig { 11 | private String privateKey; 12 | private String iss; 13 | private String kid; 14 | private String projectId; 15 | private boolean supportVoipPush = false; 16 | 17 | public String getPrivateKey() { 18 | return privateKey; 19 | } 20 | 21 | public void setPrivateKey(String privateKey) { 22 | this.privateKey = privateKey; 23 | } 24 | 25 | public String getIss() { 26 | return iss; 27 | } 28 | 29 | public void setIss(String iss) { 30 | this.iss = iss; 31 | } 32 | 33 | public String getKid() { 34 | return kid; 35 | } 36 | 37 | public void setKid(String kid) { 38 | this.kid = kid; 39 | } 40 | 41 | public String getProjectId() { 42 | return projectId; 43 | } 44 | 45 | public void setProjectId(String projectId) { 46 | this.projectId = projectId; 47 | } 48 | 49 | public boolean isSupportVoipPush() { 50 | return supportVoipPush; 51 | } 52 | 53 | public void setSupportVoipPush(boolean supportVoipPush) { 54 | this.supportVoipPush = supportVoipPush; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/HMPushService.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | 5 | public interface HMPushService { 6 | Object push(PushMessage pushMessage); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/HMPushServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import cn.wildfirechat.push.Utility; 6 | import cn.wildfirechat.push.hm.payload.AlertPayload; 7 | import cn.wildfirechat.push.hm.payload.VoipPayload; 8 | import com.auth0.jwt.JWT; 9 | import com.auth0.jwt.JWTCreator; 10 | import com.auth0.jwt.algorithms.Algorithm; 11 | import org.apache.commons.codec.binary.Base64; 12 | import org.apache.commons.io.IOUtils; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.stereotype.Service; 17 | 18 | import javax.annotation.PostConstruct; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.OutputStream; 22 | import java.net.HttpURLConnection; 23 | import java.net.URL; 24 | import java.nio.charset.Charset; 25 | import java.security.KeyFactory; 26 | import java.security.NoSuchAlgorithmException; 27 | import java.security.PrivateKey; 28 | import java.security.interfaces.RSAPrivateKey; 29 | import java.security.spec.InvalidKeySpecException; 30 | import java.security.spec.PKCS8EncodedKeySpec; 31 | import java.text.MessageFormat; 32 | import java.util.List; 33 | 34 | @Service 35 | public class HMPushServiceImpl implements HMPushService { 36 | private static final String AUD = "https://oauth-login.cloud.huawei.com/oauth2/v3/token"; 37 | private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 38 | private static final Logger LOG = LoggerFactory.getLogger(HMPushServiceImpl.class); 39 | 40 | @Autowired 41 | HMConfig config; 42 | 43 | private String pushUrl; 44 | 45 | @PostConstruct 46 | void setupPushUrl() { 47 | this.pushUrl = String.format("https://push-api.cloud.huawei.com/v3/%s/messages:send", config.getProjectId()); 48 | } 49 | 50 | private String createJwt() throws NoSuchAlgorithmException, InvalidKeySpecException { 51 | RSAPrivateKey prk = (RSAPrivateKey) getPrivateKey(config.getPrivateKey()); 52 | Algorithm algorithm = Algorithm.RSA256(null, prk); 53 | long iat = System.currentTimeMillis() / 1000; 54 | long exp = iat + 3600; 55 | JWTCreator.Builder builder = 56 | JWT.create() 57 | .withIssuer(config.getIss()) 58 | .withKeyId(config.getKid()) 59 | .withAudience(AUD) 60 | .withClaim("iat", iat) 61 | .withClaim("exp", exp); 62 | String jwt = builder.sign(algorithm); 63 | return jwt; 64 | } 65 | 66 | private PrivateKey getPrivateKey(String key) throws NoSuchAlgorithmException, InvalidKeySpecException { 67 | PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodeBase64(key)); 68 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 69 | PrivateKey privateKey = keyFactory.generatePrivate(keySpec); 70 | return privateKey; 71 | } 72 | 73 | private byte[] decodeBase64(String key) { 74 | return Base64.decodeBase64(key.getBytes(DEFAULT_CHARSET)); 75 | } 76 | 77 | 78 | @Override 79 | public Object push(PushMessage pushMessage) { 80 | try { 81 | String jwt = null; 82 | jwt = createJwt(); 83 | 84 | if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_RECALLED || pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_DELETED) { 85 | //Todo not implement 86 | //撤回或者删除消息,需要更新远程通知,暂未实现 87 | return null; 88 | } 89 | 90 | if (config.isSupportVoipPush() && (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE || pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE)) { 91 | VoipPayload voipPayload = VoipPayload.buildAlertPayload(pushMessage); 92 | String response = httpPost(this.pushUrl, jwt, 10, voipPayload.toString(), 10000, 10000); 93 | LOG.info("Push voip message to {} response {}", pushMessage.getDeviceToken(), response); 94 | } else { 95 | AlertPayload alertPayload = AlertPayload.buildAlertPayload(pushMessage); 96 | String response = httpPost(this.pushUrl, jwt, 0, alertPayload.toString(), 10000, 10000); 97 | LOG.info("Push alert message to {} response {}", pushMessage.getDeviceToken(), response); 98 | } 99 | 100 | } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) { 101 | e.printStackTrace(); 102 | } 103 | 104 | return null; 105 | } 106 | 107 | private String httpPost(String httpUrl, String jwt, int pushType, String data, int connectTimeout, int readTimeout) throws IOException { 108 | OutputStream outPut = null; 109 | HttpURLConnection urlConnection = null; 110 | InputStream in = null; 111 | 112 | try { 113 | URL url = new URL(httpUrl); 114 | urlConnection = (HttpURLConnection) url.openConnection(); 115 | urlConnection.setRequestMethod("POST"); 116 | urlConnection.setDoOutput(true); 117 | urlConnection.setDoInput(true); 118 | urlConnection.setRequestProperty("Content-Type", "application/json"); 119 | urlConnection.setRequestProperty("Authorization", "Bearer " + jwt); 120 | urlConnection.setRequestProperty("push-type", pushType + ""); 121 | urlConnection.setConnectTimeout(connectTimeout); 122 | urlConnection.setReadTimeout(readTimeout); 123 | urlConnection.connect(); 124 | 125 | // POST data 126 | outPut = urlConnection.getOutputStream(); 127 | outPut.write(data.getBytes("UTF-8")); 128 | outPut.flush(); 129 | 130 | // read response 131 | if (urlConnection.getResponseCode() < 400) { 132 | in = urlConnection.getInputStream(); 133 | } else { 134 | in = urlConnection.getErrorStream(); 135 | } 136 | 137 | List lines = IOUtils.readLines(in, urlConnection.getContentEncoding()); 138 | StringBuffer strBuf = new StringBuffer(); 139 | for (String line : lines) { 140 | strBuf.append(line); 141 | } 142 | // LOG.info(strBuf.toString()); 143 | return strBuf.toString(); 144 | } finally { 145 | IOUtils.closeQuietly(outPut); 146 | IOUtils.closeQuietly(in); 147 | if (urlConnection != null) { 148 | urlConnection.disconnect(); 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/payload/AlertPayload.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm.payload; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import cn.wildfirechat.push.Utility; 6 | import cn.wildfirechat.push.hm.payload.internal.ClickAction; 7 | import cn.wildfirechat.push.hm.payload.internal.Notification; 8 | import cn.wildfirechat.push.hm.payload.internal.Payload; 9 | import cn.wildfirechat.push.hm.payload.internal.Target; 10 | import com.google.gson.Gson; 11 | import org.json.simple.JSONObject; 12 | 13 | import java.text.MessageFormat; 14 | import java.util.ArrayList; 15 | 16 | 17 | public class AlertPayload { 18 | Payload payload; 19 | Target target; 20 | 21 | @Override 22 | public String toString() { 23 | return new Gson().toJson(this); 24 | } 25 | 26 | public static AlertPayload buildAlertPayload(PushMessage pushMessage) { 27 | Notification notification = new Notification(); 28 | String[] titleAndBody = Utility.getPushTitleAndContent(pushMessage); 29 | notification.title = titleAndBody[0]; 30 | notification.body = titleAndBody[1]; 31 | 32 | ClickAction clickAction = new ClickAction(); 33 | 34 | if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_NORMAL) { 35 | JSONObject data = new JSONObject(); 36 | JSONObject conv = new JSONObject(); 37 | conv.put("type", pushMessage.convType); 38 | conv.put("target", pushMessage.target); 39 | conv.put("line", pushMessage.line); 40 | data.put("conversation", conv); 41 | clickAction.data = data; 42 | } else { 43 | // TODO 44 | } 45 | 46 | notification.clickAction = clickAction; 47 | 48 | Target target = new Target(); 49 | target.token = new ArrayList<>(); 50 | target.token.add(pushMessage.deviceToken); 51 | 52 | AlertPayload alertPayload = new AlertPayload(); 53 | alertPayload.payload = new Payload(); 54 | alertPayload.payload.notification = notification; 55 | alertPayload.target = target; 56 | 57 | return alertPayload; 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/payload/VoipPayload.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm.payload; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.hm.payload.internal.Payload; 5 | import cn.wildfirechat.push.hm.payload.internal.Target; 6 | import com.google.gson.Gson; 7 | 8 | import java.util.ArrayList; 9 | 10 | 11 | public class VoipPayload { 12 | Payload payload; 13 | Target target; 14 | 15 | @Override 16 | public String toString() { 17 | return new Gson().toJson(this); 18 | } 19 | 20 | public static VoipPayload buildAlertPayload(PushMessage pushMessage) { 21 | Target target = new Target(); 22 | target.token = new ArrayList<>(); 23 | target.token.add(pushMessage.deviceToken); 24 | 25 | VoipPayload voipPayload = new VoipPayload(); 26 | voipPayload.payload = new Payload(); 27 | voipPayload.payload.extraData = "TODO"; 28 | voipPayload.target = target; 29 | 30 | return voipPayload; 31 | } 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/payload/internal/ClickAction.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm.payload.internal; 2 | 3 | import org.json.simple.JSONObject; 4 | 5 | public class ClickAction { 6 | /** 7 | * 0:打开应用首页 8 | *

9 | * 1:打开应用自定义页面 10 | */ 11 | public int actionType; 12 | 13 | public String action; 14 | public String uri; 15 | public JSONObject data; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/payload/internal/Notification.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm.payload.internal; 2 | 3 | public class Notification { 4 | public String category = "IM"; 5 | public String title; 6 | public String body; 7 | public ClickAction clickAction; 8 | public int style; 9 | public String image; 10 | public Integer notifyId; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/payload/internal/Payload.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm.payload.internal; 2 | 3 | public class Payload { 4 | public Notification notification; 5 | public String extraData; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/hm/payload/internal/Target.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.hm.payload.internal; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class Target { 6 | public ArrayList token; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/ios/ApnsConfig.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.ios; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix="apns") 9 | @PropertySource(value = "file:config/apns.properties") 10 | public class ApnsConfig { 11 | String cerPath; 12 | String cerPwd; 13 | 14 | String authKeyPath; 15 | String keyId; 16 | String teamId; 17 | 18 | String voipCerPath; 19 | String voipCerPwd; 20 | 21 | String alert; 22 | String voipAlert; 23 | 24 | boolean voipFeature; 25 | 26 | public String getCerPath() { 27 | return cerPath; 28 | } 29 | 30 | public void setCerPath(String cerPath) { 31 | this.cerPath = cerPath; 32 | } 33 | 34 | public String getCerPwd() { 35 | return cerPwd; 36 | } 37 | 38 | public void setCerPwd(String cerPwd) { 39 | this.cerPwd = cerPwd; 40 | } 41 | 42 | public String getVoipCerPath() { 43 | return voipCerPath; 44 | } 45 | 46 | public void setVoipCerPath(String voipCerPath) { 47 | this.voipCerPath = voipCerPath; 48 | } 49 | 50 | public String getVoipCerPwd() { 51 | return voipCerPwd; 52 | } 53 | 54 | public void setVoipCerPwd(String voipCerPwd) { 55 | this.voipCerPwd = voipCerPwd; 56 | } 57 | 58 | public String getAlert() { 59 | return alert; 60 | } 61 | 62 | public void setAlert(String alert) { 63 | this.alert = alert; 64 | } 65 | 66 | public String getVoipAlert() { 67 | return voipAlert; 68 | } 69 | 70 | public void setVoipAlert(String voipAlert) { 71 | this.voipAlert = voipAlert; 72 | } 73 | 74 | public boolean isVoipFeature() { 75 | return voipFeature; 76 | } 77 | 78 | public void setVoipFeature(boolean voipFeature) { 79 | this.voipFeature = voipFeature; 80 | } 81 | 82 | public String getAuthKeyPath() { 83 | return authKeyPath; 84 | } 85 | 86 | public void setAuthKeyPath(String authKeyPath) { 87 | this.authKeyPath = authKeyPath; 88 | } 89 | 90 | public String getKeyId() { 91 | return keyId; 92 | } 93 | 94 | public void setKeyId(String keyId) { 95 | this.keyId = keyId; 96 | } 97 | 98 | public String getTeamId() { 99 | return teamId; 100 | } 101 | 102 | public void setTeamId(String teamId) { 103 | this.teamId = teamId; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/ios/ApnsServer.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.ios; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import cn.wildfirechat.push.Utility; 6 | import com.turo.pushy.apns.*; 7 | import com.turo.pushy.apns.auth.ApnsSigningKey; 8 | import com.turo.pushy.apns.metrics.micrometer.MicrometerApnsClientMetricsListener; 9 | import com.turo.pushy.apns.util.ApnsPayloadBuilder; 10 | import com.turo.pushy.apns.util.SimpleApnsPushNotification; 11 | import com.turo.pushy.apns.util.concurrent.PushNotificationFuture; 12 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry; 13 | import org.json.simple.JSONObject; 14 | import org.json.simple.parser.JSONParser; 15 | import org.json.simple.parser.ParseException; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.stereotype.Component; 20 | import org.springframework.util.StringUtils; 21 | 22 | import javax.annotation.PostConstruct; 23 | import java.io.File; 24 | import java.util.*; 25 | 26 | @Component 27 | public class ApnsServer { 28 | private static final Logger LOG = LoggerFactory.getLogger(ApnsServer.class); 29 | 30 | final SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry(); 31 | final MicrometerApnsClientMetricsListener productMetricsListener = 32 | new MicrometerApnsClientMetricsListener(meterRegistry, 33 | "notifications", "apns_product"); 34 | final MicrometerApnsClientMetricsListener developMetricsListener = 35 | new MicrometerApnsClientMetricsListener(meterRegistry, 36 | "notifications", "apns_develop"); 37 | 38 | ApnsClient productSvc; 39 | ApnsClient developSvc; 40 | ApnsClient productVoipSvc; 41 | ApnsClient developVoipSvc; 42 | 43 | @Autowired 44 | private ApnsConfig mConfig; 45 | 46 | @PostConstruct 47 | private void init() { 48 | if (StringUtils.isEmpty(mConfig.alert)) { 49 | mConfig.alert = "default"; 50 | } 51 | 52 | if (StringUtils.isEmpty(mConfig.voipAlert)) { 53 | mConfig.alert = "default"; 54 | } 55 | 56 | try { 57 | if (!StringUtils.isEmpty(mConfig.authKeyPath) && !StringUtils.isEmpty(mConfig.keyId) && !StringUtils.isEmpty(mConfig.teamId)) { 58 | productSvc = new ApnsClientBuilder() 59 | .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST) 60 | .setSigningKey(ApnsSigningKey.loadFromPkcs8File(new File(mConfig.authKeyPath), mConfig.teamId, mConfig.keyId)) 61 | .setMetricsListener(productMetricsListener) 62 | .build(); 63 | 64 | developSvc = new ApnsClientBuilder() 65 | .setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST) 66 | .setSigningKey(ApnsSigningKey.loadFromPkcs8File(new File(mConfig.authKeyPath), mConfig.teamId, mConfig.keyId)) 67 | .setMetricsListener(developMetricsListener) 68 | .build(); 69 | 70 | if (mConfig.voipFeature) { 71 | productVoipSvc = new ApnsClientBuilder() 72 | .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST) 73 | .setSigningKey(ApnsSigningKey.loadFromPkcs8File(new File(mConfig.authKeyPath), mConfig.teamId, mConfig.keyId)) 74 | .setMetricsListener(productMetricsListener) 75 | .build(); 76 | developVoipSvc = new ApnsClientBuilder() 77 | .setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST) 78 | .setSigningKey(ApnsSigningKey.loadFromPkcs8File(new File(mConfig.authKeyPath), mConfig.teamId, mConfig.keyId)) 79 | .setMetricsListener(developMetricsListener) 80 | .build(); 81 | } 82 | } else { 83 | productSvc = new ApnsClientBuilder() 84 | .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST) 85 | .setClientCredentials(new File(mConfig.cerPath), mConfig.cerPwd) 86 | .setMetricsListener(productMetricsListener) 87 | .build(); 88 | 89 | developSvc = new ApnsClientBuilder() 90 | .setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST) 91 | .setClientCredentials(new File(mConfig.cerPath), mConfig.cerPwd) 92 | .setMetricsListener(developMetricsListener) 93 | .build(); 94 | 95 | if (mConfig.voipFeature) { 96 | productVoipSvc = new ApnsClientBuilder() 97 | .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST) 98 | .setClientCredentials(new File(mConfig.voipCerPath), mConfig.voipCerPwd) 99 | .setMetricsListener(productMetricsListener) 100 | .build(); 101 | developVoipSvc = new ApnsClientBuilder() 102 | .setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST) 103 | .setClientCredentials(new File(mConfig.voipCerPath), mConfig.voipCerPwd) 104 | .setMetricsListener(developMetricsListener) 105 | .build(); 106 | } 107 | } 108 | } catch (Exception e) { 109 | LOG.error("ApnsServer init failed"); 110 | e.printStackTrace(); 111 | } 112 | } 113 | 114 | public long getMessageId(PushMessage pushMessage) { 115 | try { 116 | JSONObject jsonObject = (JSONObject)(new JSONParser().parse(pushMessage.pushData)); 117 | if(jsonObject.get("messageUid") instanceof Long) { 118 | return (Long)jsonObject.get("messageUid"); 119 | } else if(jsonObject.get("messageUid") instanceof Integer) { 120 | return (Integer)jsonObject.get("messageUid"); 121 | } else if(jsonObject.get("messageUid") instanceof Double) { 122 | double uid = (Double)jsonObject.get("messageUid"); 123 | return (long)uid; 124 | } 125 | } catch (ParseException e) { 126 | e.printStackTrace(); 127 | } 128 | return 0; 129 | } 130 | 131 | private static class TimeUUID { 132 | UUID uuid; 133 | long timestamp; 134 | 135 | public TimeUUID(UUID uuid) { 136 | this.uuid = uuid; 137 | this.timestamp = System.currentTimeMillis(); 138 | } 139 | } 140 | 141 | private Map callPushId = new HashMap<>(); 142 | 143 | private synchronized void addCallPushId(long callId, UUID uuid) { 144 | callPushId.put(callId, new TimeUUID(uuid)); 145 | 146 | //remove history record 147 | Iterator> iterator = callPushId.entrySet().iterator(); 148 | long now = System.currentTimeMillis(); 149 | while (iterator.hasNext()) { 150 | Map.Entry entry = iterator.next(); 151 | if (now - entry.getValue().timestamp > 10*60*1000) { 152 | iterator.remove(); 153 | } 154 | } 155 | } 156 | 157 | private synchronized UUID getCallPushId(long callId) { 158 | TimeUUID timeUUID = callPushId.remove(callId); 159 | if (timeUUID != null) { 160 | return timeUUID.uuid; 161 | } 162 | return null; 163 | } 164 | 165 | public void pushMessage(PushMessage pushMessage) { 166 | ApnsClient service; 167 | String sound = mConfig.alert; 168 | 169 | String collapseId = null; 170 | if(pushMessage.messageId > 0) { 171 | collapseId = pushMessage.messageId + ""; 172 | } 173 | 174 | boolean isCallInvite = pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE; 175 | 176 | if (isCallInvite) { 177 | sound = mConfig.voipAlert; 178 | } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE || pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_ANSWER) { 179 | if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE && pushMessage.callStartUid > 0) { 180 | collapseId = pushMessage.callStartUid + ""; 181 | } 182 | sound = null; 183 | } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_RECALLED || pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_DELETED) { 184 | sound = null; 185 | long messageId = getMessageId(pushMessage); 186 | if(messageId > 0) { 187 | collapseId = messageId + ""; 188 | } 189 | } else if(pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_NORMAL && pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_SECRET_CHAT) { 190 | LOG.error("not support push message type:{}", pushMessage.pushMessageType); 191 | } 192 | 193 | int badge = pushMessage.getUnReceivedMsg(); 194 | if (badge <= 0) { 195 | badge = 1; 196 | } 197 | 198 | String[] arr = Utility.getPushTitleAndContent(pushMessage); 199 | String title = arr[0]; 200 | String body = arr[1]; 201 | 202 | final ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder(); 203 | payloadBuilder.setAlertBody(body); 204 | payloadBuilder.setAlertTitle(title); 205 | payloadBuilder.setBadgeNumber(badge); 206 | payloadBuilder.setSound(sound); 207 | JSONObject jsonObject = new JSONObject(); 208 | jsonObject.put("sender", pushMessage.sender); 209 | jsonObject.put("convType", pushMessage.convType); 210 | jsonObject.put("convTarget", pushMessage.target); 211 | jsonObject.put("convLine", pushMessage.line); 212 | jsonObject.put("contType", pushMessage.cntType); 213 | jsonObject.put("pushData", pushMessage.pushData); 214 | payloadBuilder.addCustomProperty("wfc", jsonObject); 215 | 216 | Calendar c = Calendar.getInstance(); 217 | ApnsPushNotification pushNotification; 218 | 219 | UUID apnsId = null; 220 | 221 | if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE && pushMessage.callStartUid > 0) { 222 | apnsId = getCallPushId(pushMessage.callStartUid); 223 | } 224 | 225 | if (!mConfig.voipFeature || pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE) { 226 | if (pushMessage.getPushType() == IOSPushType.IOS_PUSH_TYPE_DISTRIBUTION) { 227 | service = productSvc; 228 | } else { 229 | service = developSvc; 230 | } 231 | if(pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE || StringUtils.isEmpty(pushMessage.getVoipDeviceToken())) { 232 | c.add(Calendar.MINUTE, 10); //普通推送 233 | String payload = payloadBuilder.buildWithDefaultMaximumLength(); 234 | pushNotification = new SimpleApnsPushNotification(pushMessage.deviceToken, pushMessage.packageName, payload, c.getTime(), DeliveryPriority.CONSERVE_POWER, PushType.ALERT, collapseId, apnsId); 235 | } else { 236 | c.add(Calendar.MINUTE, 1); //voip通知,使用普通推送 237 | payloadBuilder.setContentAvailable(true); 238 | payloadBuilder.addCustomProperty("voip", true); 239 | payloadBuilder.addCustomProperty("voip_type", pushMessage.pushMessageType); 240 | payloadBuilder.addCustomProperty("voip_data", pushMessage.pushData); 241 | String payload = payloadBuilder.buildWithDefaultMaximumLength(); 242 | pushNotification = new SimpleApnsPushNotification(pushMessage.deviceToken, pushMessage.packageName, payload, c.getTime(), DeliveryPriority.IMMEDIATE, PushType.BACKGROUND, collapseId, apnsId); 243 | } 244 | } else { 245 | if (pushMessage.getPushType() == IOSPushType.IOS_PUSH_TYPE_DISTRIBUTION) { 246 | service = productVoipSvc; 247 | } else { 248 | service = developVoipSvc; 249 | } 250 | c.add(Calendar.MINUTE, 1); 251 | String payload = payloadBuilder.buildWithDefaultMaximumLength(); 252 | pushNotification = new SimpleApnsPushNotification(pushMessage.voipDeviceToken, pushMessage.packageName + ".voip", payload, c.getTime(), DeliveryPriority.IMMEDIATE, PushType.VOIP, collapseId, apnsId); 253 | } 254 | 255 | SimpleApnsPushNotification simpleApnsPushNotification = (SimpleApnsPushNotification)pushNotification; 256 | LOG.info("CollapseId:{}", simpleApnsPushNotification.getCollapseId()); 257 | 258 | if (service == null) { 259 | LOG.error("Service not exist!!!!"); 260 | return; 261 | } 262 | 263 | final PushNotificationFuture> sendNotificationFuture = service.sendNotification(pushNotification); 264 | sendNotificationFuture.addListener(future -> { 265 | // When using a listener, callers should check for a failure to send a 266 | // notification by checking whether the future itself was successful 267 | // since an exception will not be thrown. 268 | if (future.isSuccess()) { 269 | final PushNotificationResponse pushNotificationResponse = 270 | sendNotificationFuture.getNow(); 271 | if(!pushNotificationResponse.isAccepted()) { 272 | LOG.error("apns push failure: {}", pushNotificationResponse.getRejectionReason()); 273 | } else { 274 | LOG.info("push success: {}", pushNotificationResponse.getApnsId().toString()); 275 | LOG.info("token invalidate timestamp: {}", pushNotificationResponse.getTokenInvalidationTimestamp()); 276 | 277 | if (isCallInvite) { 278 | addCallPushId(pushMessage.messageId, pushNotificationResponse.getApnsId()); 279 | } 280 | } 281 | } else { 282 | // Something went wrong when trying to send the notification to the 283 | // APNs gateway. We can find the exception that caused the failure 284 | // by getting future.cause(). 285 | future.cause().printStackTrace(); 286 | LOG.error("apns push failure: {}", future.cause().getLocalizedMessage()); 287 | } 288 | }); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/ios/IOSPushService.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.ios; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | 5 | public interface IOSPushService { 6 | Object push(PushMessage pushMessage); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/ios/IOSPushServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.ios; 2 | 3 | import cn.wildfirechat.push.PushMessage; 4 | import cn.wildfirechat.push.PushMessageType; 5 | import cn.wildfirechat.push.Utility; 6 | import cn.wildfirechat.push.android.AndroidPushType; 7 | import cn.wildfirechat.push.android.getui.GetuiPush; 8 | import com.google.gson.Gson; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.SynchronousQueue; 16 | import java.util.concurrent.ThreadPoolExecutor; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | @Service 20 | public class IOSPushServiceImpl implements IOSPushService { 21 | private static final Logger LOG = LoggerFactory.getLogger(IOSPushServiceImpl.class); 22 | @Autowired 23 | public ApnsServer apnsServer; 24 | 25 | @Autowired 26 | private GetuiPush getuiPush; 27 | 28 | private ExecutorService executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() * 100, 29 | 60L, TimeUnit.SECONDS, 30 | new SynchronousQueue()); 31 | 32 | @Override 33 | public Object push(PushMessage pushMessage) { 34 | LOG.info("iOS push {}", new Gson().toJson(pushMessage)); 35 | if(Utility.filterPush(pushMessage)) { 36 | LOG.info("canceled"); 37 | return "Canceled"; 38 | } 39 | final long start = System.currentTimeMillis(); 40 | executorService.execute(()->{ 41 | long now = System.currentTimeMillis(); 42 | if (now - start > 15000) { 43 | LOG.error("等待太久,消息抛弃"); 44 | return; 45 | } 46 | if(pushMessage.pushType < 3) { 47 | apnsServer.pushMessage(pushMessage); 48 | } else if(pushMessage.pushType == AndroidPushType.ANDROID_PUSH_TYPE_GETUI) { 49 | getuiPush.push(pushMessage, false); 50 | } else { 51 | LOG.error("Unknown ios push type: {}", pushMessage.pushType); 52 | } 53 | 54 | }); 55 | return "OK"; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/cn/wildfirechat/push/ios/IOSPushType.java: -------------------------------------------------------------------------------- 1 | package cn.wildfirechat.push.ios; 2 | 3 | public interface IOSPushType { 4 | int IOS_PUSH_TYPE_DISTRIBUTION = 0; 5 | int IOS_PUSH_TYPE_DEVELOPEMENT = 1; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/libs/MiPush_SDK_Server_2_2_19.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/push_server/554618ab8cf9af501c47eea7a2f8999ae8f572ce/src/main/libs/MiPush_SDK_Server_2_2_19.jar -------------------------------------------------------------------------------- /src/main/libs/httpclient-4.5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/push_server/554618ab8cf9af501c47eea7a2f8999ae8f572ce/src/main/libs/httpclient-4.5.jar -------------------------------------------------------------------------------- /src/main/libs/httpcore-4.4.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/push_server/554618ab8cf9af501c47eea7a2f8999ae8f572ce/src/main/libs/httpcore-4.4.1.jar -------------------------------------------------------------------------------- /src/main/libs/opush-server-sdk-1.0.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/push_server/554618ab8cf9af501c47eea7a2f8999ae8f572ce/src/main/libs/opush-server-sdk-1.0.4.jar -------------------------------------------------------------------------------- /src/main/libs/vPush-server-sdk-1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildfirechat/push_server/554618ab8cf9af501c47eea7a2f8999ae8f572ce/src/main/libs/vPush-server-sdk-1.0.jar -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8085 2 | logging.level.root=debug 3 | logging.file=push.log 4 | --------------------------------------------------------------------------------