فهرست منبع

华为微瞳推送

locky 7 ماه پیش
والد
کامیت
69fed2dfb9

+ 10 - 0
Object/enums/ConstantEnum.py

@@ -0,0 +1,10 @@
+# @Author    : Rocky
+# @File      : ConstantEnum.py
+# @Time      : 2025/1/15 17:36
+from enum import Enum, unique
+
+
+@unique
+class ConstantEnum(Enum):
+    ZOSI_APP_BUNDLE_ID = 'com.ansjer.zccloud_ab'
+    VSEES_APP_BUNDLE_ID = 'com.cloudlife.commissionf_a'

+ 19 - 7
Service/DevicePushService.py

@@ -29,6 +29,7 @@ from Model.models import UidPushModel, SysMsgModel, DeviceSharePermission, Devic
 from Object.ETkObject import ETkObject
 from Object.ETkObject import ETkObject
 from Object.OCIObjectStorage import OCIObjectStorage
 from Object.OCIObjectStorage import OCIObjectStorage
 from Object.UidTokenObject import UidTokenObject
 from Object.UidTokenObject import UidTokenObject
+from Object.enums.ConstantEnum import ConstantEnum
 from Object.enums.EventTypeEnum import EventTypeEnumObj
 from Object.enums.EventTypeEnum import EventTypeEnumObj
 from Object.utils import LocalDateTimeUtil
 from Object.utils import LocalDateTimeUtil
 from Service.CommonService import CommonService
 from Service.CommonService import CommonService
@@ -37,6 +38,8 @@ from Service.HuaweiPushService.HuaweiPushService import HuaweiPushObject
 from Service.PushService import PushObject
 from Service.PushService import PushObject
 from django.db import close_old_connections
 from django.db import close_old_connections
 
 
+from Service.VSeesHuaweiPushService.VseesHuaweiPushObject import VseesHuaweiPushObject
+
 LOGGING = logging.getLogger('info')
 LOGGING = logging.getLogger('info')
 TIME_LOGGER = logging.getLogger('time')
 TIME_LOGGER = logging.getLogger('time')
 ERROR_INFO_LOGGER = logging.getLogger('error_info')
 ERROR_INFO_LOGGER = logging.getLogger('error_info')
@@ -450,8 +453,12 @@ class DevicePushService:
                         push_result = PushObject.android_jpush(**kwargs)
                         push_result = PushObject.android_jpush(**kwargs)
 
 
                 elif push_type == 3:
                 elif push_type == 3:
-                    huawei_push_object = HuaweiPushObject(appBundleId=kwargs["appBundleId"])
-                    huawei_push_object.send_push_notify_message(**push_kwargs)
+                    if kwargs["appBundleId"] == ConstantEnum.ZOSI_APP_BUNDLE_ID.value:
+                        huawei_push_object = HuaweiPushObject()
+                        huawei_push_object.send_push_notify_message(**push_kwargs)
+                    elif kwargs["appBundleId"] == ConstantEnum.VSEES_APP_BUNDLE_ID.value:
+                        vsees_huawei_push_object = VseesHuaweiPushObject()
+                        vsees_huawei_push_object.send_push_notify_message(**push_kwargs)
                 elif push_type == 4:  # android xmpush
                 elif push_type == 4:  # android xmpush
                     if kwargs['event_type'] in EventTypeEnumObj.DATA_PUSH_EVENT_TYPE_LIST.value:
                     if kwargs['event_type'] in EventTypeEnumObj.DATA_PUSH_EVENT_TYPE_LIST.value:
                         push_channel = 'push_to_talk'
                         push_channel = 'push_to_talk'
@@ -791,11 +798,16 @@ class DevicePushService:
                 push_result = PushObject.android_fcm_push_v1(
                 push_result = PushObject.android_fcm_push_v1(
                     uid, appBundleId, token_val, n_time, event_type, msg_title, msg_text, uid, channel, image_url)
                     uid, appBundleId, token_val, n_time, event_type, msg_title, msg_text, uid, channel, image_url)
             elif push_type == 3:
             elif push_type == 3:
-                huawei_push_object = HuaweiPushObject(appBundleId=appBundleId)
-                push_result = huawei_push_object.send_push_notify_message(
-                    token_val=token_val, msg_title=msg_title, msg_text=msg_text, uid=uid, event_type=event_type,
-                    n_time=n_time, image_url=image_url, channel=channel)
-
+                if appBundleId == ConstantEnum.ZOSI_APP_BUNDLE_ID.value:
+                    huawei_push_object = HuaweiPushObject()
+                    push_result = huawei_push_object.send_push_notify_message(
+                        token_val=token_val, msg_title=msg_title, msg_text=msg_text, uid=uid, event_type=event_type,
+                        n_time=n_time, image_url=image_url, channel=channel)
+                elif appBundleId == ConstantEnum.VSEES_APP_BUNDLE_ID.value:
+                    vsees_huawei_push_object = VseesHuaweiPushObject()
+                    push_result = vsees_huawei_push_object.send_push_notify_message(
+                        token_val=token_val, msg_title=msg_title, msg_text=msg_text, uid=uid, event_type=event_type,
+                        n_time=n_time, image_url=image_url, channel=channel)
             TIME_LOGGER.info('{}推送图片,push_type:{},推送结果:{}'.format(uid, push_type, push_result))
             TIME_LOGGER.info('{}推送图片,push_type:{},推送结果:{}'.format(uid, push_type, push_result))
         except Exception as e:
         except Exception as e:
             ERROR_INFO_LOGGER.error(
             ERROR_INFO_LOGGER.error(

+ 7 - 10
Service/HuaweiPushService/HuaweiPushService.py

@@ -4,14 +4,14 @@ from AnsjerPush.config import LOGGER
 from Object.enums.EventTypeEnum import EventTypeEnumObj
 from Object.enums.EventTypeEnum import EventTypeEnumObj
 from Service.HuaweiPushService import push_admin
 from Service.HuaweiPushService import push_admin
 from Service.HuaweiPushService.push_admin import messaging
 from Service.HuaweiPushService.push_admin import messaging
-from AnsjerPush.config import HUAWEI_CONFIG
+
 
 
 class HuaweiPushObject:
 class HuaweiPushObject:
     # 华为推送服务类
     # 华为推送服务类
 
 
-    def __init__(self, appBundleId='com.ansjer.zccloud_ab'):
-        self.app_id = HUAWEI_CONFIG[appBundleId]['app_id']
-        self.app_secret = HUAWEI_CONFIG[appBundleId]['app_secret']
+    def __init__(self):
+        self.app_id = '101064781'
+        self.app_secret = '29d5c5367208e35079f14779597b8f6bcc28ee39091546ed577862231fdd0fdd'
         self.init_app()
         self.init_app()
 
 
     def init_app(self):
     def init_app(self):
@@ -37,11 +37,8 @@ class HuaweiPushObject:
         """
         """
         LOGGER.info(
         LOGGER.info(
             '华为推送参数: '
             '华为推送参数: '
-            'uid:{}, token_val:{}, msg_title:{}, msg_text:{}, image_url:{}, event_type:{}, n_time:{}, channel:{}'
-            'app_id:{}, app_secret:{}'.
-            format(
-                uid, token_val, msg_title, msg_text, image_url, event_type, n_time, channel,
-                self.app_id, self.app_secret))
+            'uid:{}, token_val:{}, msg_title:{}, msg_text:{}, image_url:{}, event_type:{}, n_time:{}, channel:{}'.
+            format(uid, token_val, msg_title, msg_text, image_url, event_type, n_time, channel))
 
 
         send_succeed = self.send_notify_message(msg_title, msg_text, image_url, uid, nickname,
         send_succeed = self.send_notify_message(msg_title, msg_text, image_url, uid, nickname,
                                                 event_type, n_time, token_val, channel)
                                                 event_type, n_time, token_val, channel)
@@ -107,7 +104,7 @@ class HuaweiPushObject:
             assert (response.code == '80000000')
             assert (response.code == '80000000')
             return True
             return True
         except Exception as e:
         except Exception as e:
-            LOGGER.info('华为通知推送异常: {}'.format(repr(e)))
+            LOGGER.error('uid:{}, 华为通知推送异常: {}'.format(uid ,repr(e)))
             return False
             return False
 
 
     @staticmethod
     @staticmethod

+ 223 - 0
Service/VSeesHuaweiPushService/VseesHuaweiPushObject.py

@@ -0,0 +1,223 @@
+import json
+
+from AnsjerPush.config import LOGGER
+from Object.enums.EventTypeEnum import EventTypeEnumObj
+from Service.VSeesHuaweiPushService import push_admin
+from Service.VSeesHuaweiPushService.push_admin import messaging
+
+
+class VseesHuaweiPushObject:
+    # 华为推送服务类
+
+    def __init__(self):
+        self.app_id = '108703647'
+        self.app_secret = '6bc5b0b0ab4588fcd667b3e6c1ab4fd5d857de21ad69ee9d46e077b9f5f24e8f'
+        self.init_app()
+
+    def init_app(self):
+        """init sdk app"""
+        push_admin.initialize_app(self.app_id, self.app_secret)
+
+    def send_push_notify_message(self, token_val, msg_title, msg_text, image_url=None, uid='', nickname='', n_time='',
+                                 event_type='0', channel='', app_bundle_id='', appBundleId=''):
+        """
+        发送推送消息
+        @param token_val: 手机推送token
+        @param msg_title: 标题
+        @param msg_text: 内容
+        @param image_url: 图片链接
+        @param uid: uid
+        @param nickname: 设备昵称
+        @param n_time: 当前时间
+        @param event_type: 事件类型
+        @param channel: 通道
+        @param app_bundle_id: APP包id
+        @param appBundleId: APP包id
+        @return: bool
+        """
+        LOGGER.info(
+            '华为推送参数(微瞳): '
+            'uid:{}, token_val:{}, msg_title:{}, msg_text:{}, image_url:{}, event_type:{}, n_time:{}, channel:{}'.
+            format(uid, token_val, msg_title, msg_text, image_url, event_type, n_time, channel))
+
+        send_succeed = self.send_notify_message(msg_title, msg_text, image_url, uid, nickname,
+                                                event_type, n_time, token_val, channel)
+        if int(event_type) in EventTypeEnumObj.DATA_PUSH_EVENT_TYPE_LIST.value:
+            self.send_data_message(uid, event_type, n_time, token_val, channel)
+
+        return send_succeed
+
+    def send_notify_message(
+            self, msg_title, msg_text, image_url, uid, nickname, event_type, n_time, token_val, channel):
+        """
+        发送通知推送
+        @param msg_title:
+        @param msg_text:
+        @param image_url:
+        @param uid:
+        @param nickname:
+        @param event_type:
+        @param n_time:
+        @param token_val:
+        @param channel:
+        @return: bool
+        """
+        LOGGER.info('{}进入发送通知推送函数(微瞳)'.format(uid))
+        msg_title = '设备昵称: {}'.format(msg_title)
+        notification = messaging.Notification(
+            title=msg_title,
+            body=msg_text,
+            image=image_url
+        )
+
+        # 自定义键值对
+        data = {
+            'alert': msg_text, 'msg': '', 'sound': 'sound.aif', 'zpush': '1', 'uid': uid, 'nickname': nickname,
+            'event_type': event_type, 'received_at': n_time, 'event_time': n_time, 'channel': channel
+        }
+        data = json.dumps(data)
+        # 推送通知内容配置
+        intent = 'intent://com.vivo.pushvideo/detail?#Intent;scheme=vpushscheme;launchFlags=0x10000000;S.uid={};S.event_type={};S.event_time={};end'.format(
+            uid, event_type, n_time)
+        android_notification = self.android_notification(msg_title, msg_text, intent)
+        # 微瞳消息类型暂时改为音视频通话
+        category = 'VOIP' if self.app_id == '108703647' else 'DEVICE_REMINDER'
+        # 安卓配置
+        android = messaging.AndroidConfig(
+            data=data,
+            collapse_key=-1,
+            urgency=messaging.AndroidConfig.NORMAL_PRIORITY,
+            ttl='10000s',
+            bi_tag='the_sample_bi_tag_for_receipt_service',
+            notification=android_notification,
+            category=category
+        )
+
+        message = messaging.Message(
+            notification=notification,
+            android=android,
+            token=[token_val]
+        )
+
+        try:
+            import certifi
+            response = messaging.send_message(message, verify_peer=certifi.where())
+            LOGGER.info('{}华为通知推送响应(微瞳): {}'.format(uid, json.dumps(vars(response))))
+            assert (response.code == '80000000')
+            return True
+        except Exception as e:
+            LOGGER.info('华为通知推送异常(微瞳): {}'.format(repr(e)))
+            return False
+
+    @staticmethod
+    def send_data_message(uid, event_type, n_time, token_val, channel):
+        """
+        发送透传推送
+        @param uid:
+        @param event_type:
+        @param n_time:
+        @param token_val:
+        @param channel:
+        @return: None
+        """
+        LOGGER.info('{}进入发送透传推送函数(微瞳)'.format(uid))
+        data = {
+            'uid': uid, 'event_type': event_type, 'event_time': n_time, 'channel': channel
+        }
+        data = json.dumps(data)
+        android = messaging.AndroidConfig(
+            collapse_key=-1,
+            urgency=messaging.AndroidConfig.HIGH_PRIORITY,
+            ttl='10000s',
+            bi_tag='the_sample_bi_tag_for_receipt_service'
+        )
+
+        message = messaging.Message(
+            data=data,
+            android=android,
+            token=[token_val]
+        )
+
+        try:
+            import certifi
+            response = messaging.send_message(message, verify_peer=certifi.where())
+            LOGGER.info('{}华为透传推送响应(微瞳): {}'.format(uid, json.dumps(vars(response))))
+            assert (response.code == '80000000')
+        except Exception as e:
+            LOGGER.info('华为透传推送异常(微瞳): {}'.format(repr(e)))
+
+    @staticmethod
+    def android_notification(msg_title, msg_text, intent):
+        return messaging.AndroidNotification(
+            icon='/raw/ic_launcher2',
+            color='#AACCDD',
+            sound='/raw/shake',
+            default_sound=True,
+            click_action=messaging.AndroidClickAction(
+                action_type=1,
+                intent=intent
+            ),
+            body_loc_key='M.String.body',
+            body_loc_args=('boy', 'dog'),
+            title_loc_key='M.String.title',
+            title_loc_args=['Girl', 'Cat'],
+            channel_id='1',
+            notify_summary='',
+            multi_lang_key={'title_key': {'en': 'value1'}, 'body_key': {'en': 'value2'}},
+            style=1,
+            big_title=msg_title,
+            big_body=msg_text,
+            auto_clear=86400000,
+            importance=messaging.AndroidNotification.PRIORITY_HIGH,
+            light_settings=messaging.AndroidLightSettings(color=messaging.AndroidLightSettingsColor(
+                alpha=0, red=0, green=1, blue=1), light_on_duration='3.5', light_off_duration='5S'),
+            badge=messaging.AndroidBadgeNotification(
+                add_num=1, clazz='Classic'),
+            visibility=messaging.AndroidNotification.PUBLIC,
+            foreground_show=False,
+            # buttons=[
+            #     {'name': '接听', 'action_type': 1},
+            #     {'name': '拒绝', 'action_type': 3}
+            # ]
+        )
+
+    @staticmethod
+    def huawei_transparent_transmission(nickname, app_bundle_id, token_val, n_time, event_type, msg_title,
+                                        msg_text, user_id):
+        """
+        发送透传推送
+        @param nickname:
+        @param app_bundle_id:
+        @param event_type:
+        @param n_time:
+        @param token_val:
+        @param msg_title:
+        @param msg_text:
+        @param user_id:
+        @return: None
+        """
+        data = {
+            'nickname': nickname, 'event_type': event_type, 'event_time': n_time, 'msg_title': msg_title,
+            'msg_text': msg_text
+        }
+        data = json.dumps(data)
+        android = messaging.AndroidConfig(
+            collapse_key=-1,
+            urgency=messaging.AndroidConfig.HIGH_PRIORITY,
+            ttl='10000s',
+            bi_tag='the_sample_bi_tag_for_receipt_service'
+        )
+
+        message = messaging.Message(
+            data=data,
+            android=android,
+            token=[token_val]
+        )
+
+        try:
+            import certifi
+            response = messaging.send_message(message, verify_peer=certifi.where())
+            LOGGER.info('{}退出登录,华为透传推送响应(微瞳): {}'.format(user_id, json.dumps(vars(response))))
+            assert (response.code == '80000000')
+        except Exception as e:
+            LOGGER.info('{}退出登录,华为透传推送异常(微瞳): {}'.format(user_id, repr(e)))

+ 71 - 0
Service/VSeesHuaweiPushService/push_admin/__init__.py

@@ -0,0 +1,71 @@
+# -*-coding:utf-8-*-
+#
+# Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Huawei Admin SDK for Python."""
+
+import threading
+from Service.VSeesHuaweiPushService.push_admin import _app
+
+_apps = {}
+_apps_lock = threading.RLock()
+_DEFAULT_APP_NAME = 'DEFAULT'
+
+
+def initialize_app(appid_at, appsecret_at, appid_push=None, token_server='https://oauth-login.cloud.huawei.com/oauth2/v3/token',
+                   push_open_url='https://push-api.cloud.huawei.com'):
+    """
+        Initializes and returns a new App instance.
+        :param appid_at: appid parameters obtained by developer alliance applying for Push service
+        :param appsecret_at: appsecret parameters obtained by developer alliance applying for Push service
+        :param appid_push: the application Id in the URL
+        :param token_server: Oauth server URL
+        :param push_open_url: push open API URL
+    """
+    app = _app.App(appid_at, appsecret_at, appid_push, token_server=token_server, push_open_url=push_open_url)
+
+    with _apps_lock:
+        if appid_at not in _apps:
+            _apps[appid_at] = app
+
+        """set default app instance"""
+        if _apps.get(_DEFAULT_APP_NAME) is None:
+            _apps[_DEFAULT_APP_NAME] = app
+
+
+def get_app(appid=None):
+    """
+        get app instance
+        :param appid: appid parameters obtained by developer alliance applying for Push service
+        :return: app instance
+        Raise: ValueError
+    """
+    if appid is None:
+        with _apps_lock:
+            app = _apps.get(_DEFAULT_APP_NAME)
+            if app is None:
+                raise ValueError('The default Huawei app is not exists. '
+                                 'This means you need to call initialize_app() it.')
+            return app
+
+    with _apps_lock:
+        if appid not in _apps:
+            raise ValueError('Huawei app id[{0}] is not exists. '
+                             'This means you need to call initialize_app() it.'.format(appid))
+
+        app = _apps.get(appid)
+        if app is None:
+            raise ValueError('The app id[{0}] is None.'.format(appid))
+        return app

+ 190 - 0
Service/VSeesHuaweiPushService/push_admin/_app.py

@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import time
+import urllib
+import urllib.parse
+
+from Service.VSeesHuaweiPushService.push_admin import _http
+from Service.VSeesHuaweiPushService.push_admin import _message_serializer
+
+
+class App(object):
+    """application for HW Cloud Message(HCM)"""
+
+    JSON_ENCODER = _message_serializer.MessageSerializer()
+
+    @classmethod
+    def _send_to_server(cls, headers, body, url, verify_peer=False):
+        try:
+            msg_body = json.dumps(body)
+            response = _http.post(url, msg_body, headers, verify_peer)
+
+            if response.status_code is not 200:
+                raise ApiCallError('http status code is {0} in send.'.format(response.status_code))
+
+            # json text to dict
+            resp_dict = json.loads(response.text)
+            return resp_dict
+
+        except Exception as e:
+            raise ApiCallError('caught exception when send. {0}'.format(e))
+
+    def __init__(self, appid_at, app_secret_at, appid_push, token_server='https://oauth-login.cloud.huawei.com/oauth2/v3/token',
+                 push_open_url='https://push-api.cloud.huawei.com'):
+        """class init"""
+        self.app_id_at = appid_at
+        self.app_secret_at = app_secret_at
+        if appid_push is None:
+            self.appid_push = appid_at
+        else:
+            self.appid_push = appid_push
+        self.token_expired_time = 0
+        self.access_token = None
+        self.token_server = token_server
+        self.push_open_url = push_open_url
+        self.hw_push_server = self.push_open_url + "/v1/{0}/messages:send"
+        self.hw_push_topic_sub_server = self.push_open_url + "/v1/{0}/topic:subscribe"
+        self.hw_push_topic_unsub_server = self.push_open_url + "/v1/{0}/topic:unsubscribe"
+        self.hw_push_topic_query_server = self.push_open_url + "/v1/{0}/topic:list"
+
+    def _refresh_token(self, verify_peer=False):
+        """refresh access token
+        :param verify_peer: (optional) Either a boolean, in which case it controls whether we verify
+            the server's TLS certificate, or a string, in which case it must be a path
+            to a CA bundle to use. Defaults to ``True``.
+        """
+        headers = dict()
+        headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'
+
+        params = dict()
+        params['grant_type'] = 'client_credentials'
+        params['client_secret'] = self.app_secret_at
+        params['client_id'] = self.app_id_at
+
+        msg_body = urllib.parse.urlencode(params)
+
+        try:
+            response = _http.post(self.token_server, msg_body, headers, verify_peer=verify_peer)
+
+            if response.status_code is not 200:
+                return False, 'http status code is {0} in get access token'.format(response.status_code)
+
+            """ json string to directory """
+            response_body = json.loads(response.text)
+
+            self.access_token = response_body.get('access_token')
+            self.token_expired_time = int(round(time.time() * 1000)) + (int(response_body.get('expires_in')) - 5 * 60) * 1000
+
+            return True, None
+        except Exception as e:
+            raise ApiCallError(format(repr(e)))
+
+    def _is_token_expired(self):
+        """is access token expired"""
+        if self.access_token is None:
+            """ need refresh token """
+            return True
+        return int(round(time.time() * 1000)) >= self.token_expired_time
+
+    def _update_token(self, verify_peer=False):
+        """
+        :param verify_peer: (optional) Either a boolean, in which case it controls whether we verify
+            the server's TLS certificate, or a string, in which case it must be a path
+            to a CA bundle to use. Defaults to ``True``.
+        :return:
+        """
+        if self._is_token_expired() is True:
+            result, reason = self._refresh_token(verify_peer)
+            if result is False:
+                raise ApiCallError(reason)
+
+    def _create_header(self):
+        headers = dict()
+        headers['Content-Type'] = 'application/json;charset=utf-8'
+        headers['Authorization'] = 'Bearer {0}'.format(self.access_token)
+        return headers
+
+    def send(self, message, validate_only, **kwargs):
+        """
+            Sends the given message Huawei Cloud Messaging (HCM)
+            :param message: JSON format message
+            :param validate_only: validate message format or not
+            :param kwargs:
+                   verify_peer: HTTPS server identity verification, use library 'certifi'
+            :return:
+                response dict: response body dict
+            :raise:
+                ApiCallError: failure reason
+        """
+        verify_peer = kwargs['verify_peer']
+        self._update_token(verify_peer)
+        headers = self._create_header()
+        url = self.hw_push_server.format(self.appid_push)
+        msg_body_dict = dict()
+        msg_body_dict['validate_only'] = validate_only
+        msg_body_dict['message'] = App.JSON_ENCODER.default(message)
+
+        return App._send_to_server(headers, msg_body_dict, url, verify_peer)
+
+    def subscribe_topic(self, topic, token_list):
+        """
+        :param topic: The specific topic
+        :param token_list: The token list to be added
+        :return:
+        """
+        self._update_token()
+        headers = self._create_header()
+        url = self.hw_push_topic_sub_server.format(self.appid_push)
+        msg_body_dict = {'topic': topic, 'tokenArray': token_list}
+        return App._send_to_server(headers, msg_body_dict, url)
+
+    def unsubscribe_topic(self, topic, token_list):
+        """
+
+        :param topic: The specific topic
+        :param token_list: The token list to be deleted
+        :return:
+        """
+        self._update_token()
+        headers = self._create_header()
+        url = self.hw_push_topic_unsub_server.format(self.appid_push)
+        msg_body_dict = {'topic': topic, 'tokenArray': token_list}
+        return App._send_to_server(headers, msg_body_dict, url)
+
+    def query_subscribe_list(self, token):
+        """
+        :param token:  The specific token
+        :return:
+        """
+        self._update_token()
+        headers = self._create_header()
+        url = self.hw_push_topic_query_server.format(self.appid_push)
+        msg_body_dict = {'token': token}
+        return App._send_to_server(headers, msg_body_dict, url)
+
+
+class ApiCallError(Exception):
+    """Represents an Exception encountered while invoking the HCM API.
+
+    Attributes:
+        message: A error message string.
+        detail: Original low-level exception.
+    """
+    def __init__(self, message, detail=None):
+        Exception.__init__(self, message)
+        self.detail = detail

+ 55 - 0
Service/VSeesHuaweiPushService/push_admin/_http.py

@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import requests
+
+
+def post(url, req_body, headers=None, verify_peer=False):
+    """ post http request to slb service
+        :param url: url path
+        :param req_body: http request body
+        :param headers: http headers
+        :param verify_peer:  (optional) Either a boolean, in which case it controls whether we verify
+            the server's TLS certificate, or a string, in which case it must be a path
+            to a CA bundle to use. Defaults to ``True``.
+        :return:
+            success return response
+            fali return None
+    """
+    try:
+        response = requests.post(url, data=req_body, headers=headers, timeout=10, verify=verify_peer)
+        return response
+
+    except Exception as e:
+        raise ValueError('caught exception when post {0}. {1}'.format(url, e))
+
+
+def _format_http_text(method, url, headers, body):
+    """
+    print http head and body for request or response
+
+    For examples: _format_http_text('', title, response.headers, response.text)
+    """
+    result = method + ' ' + url + '\n'
+
+    if headers is not None:
+        for key, value in headers.items():
+            result = result + key + ': ' + value + '\n'
+
+    result = result + body
+    return result
+
+

+ 615 - 0
Service/VSeesHuaweiPushService/push_admin/_message_serializer.py

@@ -0,0 +1,615 @@
+# -*-coding:utf-8-*-
+#
+# Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+from Service.VSeesHuaweiPushService.push_admin import _messages
+import six
+
+
+class MessageSerializer(json.JSONEncoder):
+    """
+    Use https://docs.python.org/3/library/json.html to do serialization
+
+    The serializer should serialize the following messages:
+    _messages.Message
+    _messages.Notification
+    _messages.ApnsConfig
+    _messages.WebPushConfig
+    _messages.WebPushNotification
+    _messages.WebPushNotificationAction
+    _messages.WebPushHMSOptions
+    _messages.AndroidConfig
+    _messages.AndroidNotification
+    _messages.AndroidClickAction
+    _messages.BadgeNotification
+    """
+    def default(self, message):
+        """
+        :param message: The push message
+        :return: formatted push messages
+        """
+        result = {
+            'data': message.data,
+            'notification': MessageSerializer.encode_notification(message.notification),
+            'android': MessageSerializer.encode_android_config(message.android),
+            'apns': MessageSerializer.encode_apns_config(message.apns),
+            'webpush': MessageSerializer.encode_webpush_config(message.web_push),
+            'token': message.token,
+            'topic': message.topic,
+            'condition': message.condition
+        }
+        result = MessageSerializer.remove_null_values(result)
+        return result
+
+    @classmethod
+    def remove_null_values(cls, dict_value):
+        return {k: v for k, v in dict_value.items() if v not in [None, [], {}]}
+
+    @classmethod
+    def encode_notification(cls, notification):
+        """
+            An example:
+           {
+              "title":"Big News",
+              "body":"This is a Big News!",
+              "image":"https://res.vmallres.com/pimages//common/config/logo/SXppnESYv4K11DBxDFc2_0.png"
+            }
+        :param notification:
+        :return:
+        """
+        if notification is None:
+            return None
+
+        if not isinstance(notification, _messages.Notification):
+            raise ValueError('Message.notification must be an instance of Notification class.')
+
+        result = {
+            'title': notification.title,
+            'body': notification.body,
+            'image': notification.image
+        }
+        return cls.remove_null_values(result)
+
+    @classmethod
+    def encode_android_config(cls, android_config):
+        """
+        An example:
+        {
+          "android":{
+            "collapse_key":-1,
+            "urgency":"HIGH",
+            "ttl":"1448s",
+            "bi_tag":"Trump",
+            "fast_app_target":1,
+            "notification": {}
+        }
+        :param android_config:
+        :return:
+        """
+        if android_config is None:
+            return None
+
+        if not isinstance(android_config, _messages.AndroidConfig):
+            raise ValueError('Message.android must be an instance of AndroidConfig class.')
+
+        result = {
+            'collapse_key': android_config.collapse_key,
+            'urgency': android_config.urgency,
+            'ttl': android_config.ttl,
+            'bi_tag': android_config.bi_tag,
+            'fast_app_target': android_config.fast_app_target,
+            'data': android_config.data,
+            'notification': MessageSerializer.encode_android_notification(android_config.notification),
+            'category': android_config.category
+        }
+        return cls.remove_null_values(result)
+
+    @classmethod
+    def encode_android_notification(cls, notification):
+        """
+           "notification":{
+                "title":"Noti in Noti title",
+                "body":"Noti in Noti body",
+                "icon":"https://res.vmallres.com/pimages//common/config/logo/SXppnESYv4K11DBxDFc2.png",
+                "color":"#AACCDD",
+                "default_sound":true,
+                "tag":"tagBoom",
+                "importance":"PRIORITY_HIGH",
+                "click_action":{
+                    "type":2,
+                    "url":"https://www.huawei.com"
+                },
+                "body_loc_key":"M.String.body",
+                "body_loc_args":[
+                    "Boy",
+                    "Dog"
+                ],
+                "title_loc_key":"M.String.title",
+                "title_loc_args":[
+                    "Girl",
+                    "Cat"
+                ],
+                "channel_id":"RingRing",
+                "notify_summary":"Some Summary",
+                "style":2,
+                "big_title":"Big Boom Title",
+                "big_body":"Big Boom Body",
+                "notify_id":486,
+                "group":"Espace",
+                "badge":{
+                    "add_num":99,
+                    "set_num":99,
+                    "class":"Classic"
+                },
+                "ticker":"i am a ticker",
+                "auto_cancel":false,
+                "when":"2019-11-05",
+                "use_default_vibrate":true,
+                "use_default_light":false,
+                "visibility":"PUBLIC",
+                "vibrate_config":["1.5","2","3"],
+                "light_settings":{
+                    "color":{
+                        "alpha":0,
+                        "red":0,
+                        "green":1,
+                        "blue":1
+                    },
+                    "light_on_duration":"3.5",
+                    "light_off_duration":"5S"
+                },
+                "foreground_show":true
+              }
+        :param notification:
+        :return:
+        """
+        if notification is None:
+            return None
+
+        if not isinstance(notification, _messages.AndroidNotification):
+            raise ValueError('Message.AndroidConfig.notification must be an instance of AndroidNotification class.')
+
+        result = {
+            "title": notification.title,
+            "body": notification.body,
+            "icon": notification.icon,
+            "color": notification.color,
+            "sound": notification.sound,
+            "default_sound": notification.default_sound,
+            "tag": notification.tag,
+            "importance": notification.importance,
+            "multi_lang_key": notification.multi_lang_key,
+            "click_action": MessageSerializer.encode_android_click_action(notification.click_action),
+            "body_loc_key": notification.body_loc_key,
+            "body_loc_args": notification.body_loc_args,
+            "title_loc_key": notification.title_loc_key,
+            "title_loc_args": notification.title_loc_args,
+            "channel_id": notification.channel_id,
+            "notify_summary": notification.notify_summary,
+            "image": notification.image,
+            "style": notification.style,
+            "big_title": notification.big_title,
+            "big_body": notification.big_body,
+            "notify_id": notification.notify_id,
+            "group": notification.group,
+            "badge": MessageSerializer.encode_android_badge(notification.badge),
+            "ticker": notification.ticker,
+            "auto_cancel": notification.auto_cancel,
+            "when": notification.when,
+            "use_default_vibrate": notification.use_default_vibrate,
+            "use_default_light": notification.use_default_light,
+            "visibility": notification.visibility,
+            "vibrate_config": notification.vibrate_config,
+            "light_settings": MessageSerializer.encode_android_light_settings(notification.light_settings),
+            "foreground_show": notification.foreground_show,
+            "buttons": notification.buttons
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_android_click_action(cls, click_action):
+        """
+            "click_action":{
+                    "type":2,
+                    "url":"https://www.huawei.com"
+             }
+
+             "click_action":{
+                    "type":1,
+                    "intent":"https://www.huawei.com",
+                    "action":""
+             }
+
+        :param click_action: _messages.AndroidClickAction
+        :return:
+        """
+        if click_action is None:
+            return None
+
+        if not isinstance(click_action, _messages.AndroidClickAction):
+            raise ValueError('Message.AndroidConfig.AndroidNotification.click_action must be an instance\
+                             of AndroidClickAction class.')
+
+        result = {
+            "type": click_action.action_type,
+            "intent": click_action.intent,
+            "url": click_action.url,
+            "action": click_action.action
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_android_badge(cls, badge):
+        """
+        refer to: _messages.AndroidBadgeNotification
+
+        "badge":{
+                    "add_num":99,
+                    "set_num":99,
+                    "class":"Classic"
+                }
+
+        :param badge:
+        :return:
+        """
+        if badge is None:
+            return None
+
+        if not isinstance(badge, _messages.AndroidBadgeNotification):
+            raise ValueError('Message.AndroidConfig.AndroidNotification.badge must be an instance\
+                             of AndroidBadgeNotification class.')
+
+        result = {
+            "add_num": badge.add_num,
+            "set_num": badge.set_num,
+            "class": badge.clazz
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_android_light_settings(cls, android_light_settings):
+        """
+        refer to: _messages.AndroidLightSettings
+
+        "light_settings":{
+                "color":{
+                        "alpha":0,
+                        "red":0,
+                        "green":1,
+                        "blue":1
+                },
+                "light_on_duration":"3.5",
+                "light_off_duration":"5S"
+         }
+
+        :param android_light_settings:  _messages.AndroidLightSettings
+        :return:
+        """
+        if android_light_settings is None:
+            return None
+
+        if not isinstance(android_light_settings, _messages.AndroidLightSettings):
+            raise ValueError('Message.AndroidConfig.AndroidNotification.android_light_settings must be an instance\
+                             of AndroidLightSettings class.')
+
+        result = {
+            "color": MessageSerializer.encode_android_light_settings_color(android_light_settings.color),
+            "light_on_duration": android_light_settings.light_on_duration,
+            "light_off_duration": android_light_settings.light_off_duration
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_android_light_settings_color(cls, color):
+        """
+        "color":{
+                        "alpha":0,
+                        "red":0,
+                        "green":1,
+                        "blue":1
+        }
+
+        :param color: _messages.AndroidLightSettingsColor
+        :return:
+        """
+        if color is None:
+            return None
+
+        if not isinstance(color, _messages.AndroidLightSettingsColor):
+            raise ValueError('Message.AndroidConfig.AndroidNotification.android_light_settings.color must be an instance\
+                             of AndroidLightSettingsColor class.')
+        result = {
+            "alpha": color.alpha,
+            "red": color.red,
+            "green": color.green,
+            "blue": color.blue
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_webpush_config(cls, webpush_config):
+        """
+        "webpush":{
+            "headers":{
+                ...
+            },
+            "notification":{
+                ...
+            },
+            "hms_options":{
+                ...
+            }
+         }
+        :param webpush_config: refer to _messages.WebPushConfig
+        :return:
+        """
+        if webpush_config is None:
+            return None
+
+        if not isinstance(webpush_config, _messages.WebPushConfig):
+            raise ValueError('Message.webpush must be an instance of WebPushConfig class.')
+
+        result = {
+            "headers": MessageSerializer.encode_webpush_config_headers(webpush_config.headers),
+            "notification": MessageSerializer.encode_webpush_config_notification(webpush_config.notification),
+            "hms_options": MessageSerializer.encode_webpush_config_hms_options(webpush_config.hms_options),
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_webpush_config_headers(cls, webpush_headers):
+        """
+        "headers":{
+                "ttl":"990",
+                "urgency":"very-low",
+                "topic":"12313ceshi"
+            }
+
+        :param webpush_headers: _messages.WebPushHeader
+        :return:
+        """
+        if webpush_headers is None:
+            return None
+
+        if not isinstance(webpush_headers, _messages.WebPushHeader):
+            raise ValueError('Message.webpush.headers must be an instance of WebPushHeader class.')
+
+        result = {
+            "ttl": webpush_headers.ttl,
+            "urgency": webpush_headers.urgency,
+            "topic": webpush_headers.topic,
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_webpush_config_notification(cls, webpush_notification):
+        """
+        "notification":{
+                "title":"notication string",
+                "body":"web push body",
+                "actions":[
+                    {
+                        "action":"",
+                        "icon":"https://res.vmallres.com/pimages//common/config/logo/SXppnESYv4K11DBxDFc2.png",
+                        "title":"string"
+                    }
+                ],
+                "badge":"string",
+                "dir":"auto",
+                "icon":"https://res.vmallres.com/pimages//common/config/logo/SXppnESYv4K11DBxDFc2.png",
+                "image":"string",
+                "lang":"string",
+                "renotify":true,
+                "requireInteraction":true,
+                "silent":true,
+                "tag":"string",
+                "timestamp":1545201266,
+                "vibrate":[1,2,3]
+            }
+
+        :param webpush_notification: refer to _messages.WebPushNotification
+        :return:
+        """
+        if webpush_notification is None:
+            return None
+
+        if not isinstance(webpush_notification, _messages.WebPushNotification):
+            raise ValueError('Message.webpush.notification must be an instance of WebPushNotification class.')
+
+        result = {
+            "title": webpush_notification.title,
+            "body": webpush_notification.body,
+            "actions": [MessageSerializer.encode_webpush_notification_action(_)
+                        for _ in webpush_notification.actions],
+            "badge": webpush_notification.badge,
+            "dir": webpush_notification.dir,
+            "icon": webpush_notification.icon,
+            "image": webpush_notification.image,
+            "lang": webpush_notification.lang,
+            "renotify": webpush_notification.renotify,
+            "require_interaction": webpush_notification.require_interaction,
+            "silent": webpush_notification.silent,
+            "tag": webpush_notification.tag,
+            "timestamp": webpush_notification.timestamp,
+            "vibrate": webpush_notification.vibrate
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_webpush_notification_action(cls, webpush_notification_action):
+        """
+        "actions":[
+                    {
+                        "action":"",
+                        "icon":"https://res.vmallres.com/pimages//common/config/logo/SXppnESYv4K11DBxDFc2.png",
+                        "title":"string"
+                    }
+                ],
+        :param webpush_notification_action: refer to _messages.WebPushNotificationAction
+        :return:
+        """
+        if webpush_notification_action is None:
+            return None
+
+        if not isinstance(webpush_notification_action, _messages.WebPushNotificationAction):
+            raise ValueError('Message.webpush.notification.action must be an instance of \
+                            WebPushNotificationAction class.')
+
+        result = {
+            "action": webpush_notification_action.action,
+            "icon": webpush_notification_action.icon,
+            "title": webpush_notification_action.title
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_webpush_config_hms_options(cls, webpush_hms_options):
+        """
+        "hms_options":{
+                "link":"https://www.huawei.com/"
+         }
+
+        :param webpush_hms_options: refer to _messages.WebPushHMSOptions
+        :return:
+        """
+        if webpush_hms_options is None:
+            return None
+
+        if not isinstance(webpush_hms_options, _messages.WebPushHMSOptions):
+            raise ValueError('Message.webpush.hmsoptions must be an instance of \
+                            WebPushHMSOptions class.')
+
+        result = {
+            "link": webpush_hms_options.link
+        }
+        result = cls.remove_null_values(result)
+        return result
+
+    @classmethod
+    def encode_apns_config(cls, apns_config):
+        """
+        Encode APNs config into JSON
+        :param apns_config:
+        :return:
+        """
+        if apns_config is None:
+            return None
+        if not isinstance(apns_config, _messages.APNsConfig):
+            raise ValueError('Message.apns_config must be an instance of _messages.APNsConfig class.')
+
+        result = {
+            'headers': apns_config.headers,
+            'payload': cls.encode_apns_payload(apns_config.payload),
+            'hms_options': cls.encode_apns_hms_options(apns_config.apns_hms_options)
+        }
+        return cls.remove_null_values(result)
+
+    @classmethod
+    def encode_apns_payload(cls, apns_payload):
+        """Encodes an ``APNSPayload`` instance into JSON."""
+        if apns_payload is None:
+            return None
+        if not isinstance(apns_payload, _messages.APNsPayload):
+            raise ValueError('APNSConfig.payload must be an instance of _messages.APNsPayload class.')
+        result = {
+            'aps': cls.encode_apns_payload_aps(apns_payload.aps)
+        }
+        for key, value in apns_payload.custom_data.items():
+            result[key] = value
+        return cls.remove_null_values(result)
+
+    @classmethod
+    def encode_apns_payload_aps(cls, apns_payload_aps):
+        """Encodes an ``Aps`` instance into JSON."""
+        if not isinstance(apns_payload_aps, _messages.APNsAps):
+            raise ValueError('APNSPayload.aps must be an instance of _messages.APNsAps class.')
+
+        result = {
+            'alert': cls.encode_apns_payload_alert(apns_payload_aps.alert),
+            'badge': apns_payload_aps.badge,
+            'sound': apns_payload_aps.sound,
+            'category': apns_payload_aps.category,
+            'thread-id': apns_payload_aps.thread_id
+        }
+
+        if apns_payload_aps.content_available is True:
+            result['content-available'] = 1
+        if apns_payload_aps.mutable_content is True:
+            result['mutable-content'] = 1
+        if apns_payload_aps.custom_data is not None:
+            if not isinstance(apns_payload_aps.custom_data, dict):
+                raise ValueError('Aps.custom_data must be a dict.')
+            for key, val in apns_payload_aps.custom_data.items():
+                if key in result:
+                    raise ValueError('Multiple specifications for {0} in Aps.'.format(key))
+                result[key] = val
+        return cls.remove_null_values(result)
+
+    @classmethod
+    def encode_apns_payload_alert(cls, apns_payload_alert):
+        """Encodes an ``ApsAlert`` instance into JSON."""
+        if apns_payload_alert is None:
+            return None
+        if isinstance(apns_payload_alert, six.string_types):
+            return apns_payload_alert
+        if not isinstance(apns_payload_alert, _messages.APNsAlert):
+            raise ValueError('Aps.alert must be a string or an instance of _messages.APNsAlert class.')
+        result = {
+            'title': apns_payload_alert.title,
+            'body': apns_payload_alert.body,
+            'title-loc-key': apns_payload_alert.title_loc_key,
+            'title-loc-args': apns_payload_alert.title_loc_args,
+            'loc-key': apns_payload_alert.loc_key,
+            'loc-args': apns_payload_alert.loc_args,
+            'action-loc-key': apns_payload_alert.action_loc_key,
+            'launch-image': apns_payload_alert.launch_image
+        }
+        if result.get('loc-args') and not result.get('loc-key'):
+            raise ValueError(
+                'ApsAlert.loc_key is required when specifying loc_args.')
+        if result.get('title-loc-args') and not result.get('title-loc-key'):
+            raise ValueError(
+                'ApsAlert.title_loc_key is required when specifying title_loc_args.')
+        if apns_payload_alert.custom_data is not None:
+            if not isinstance(apns_payload_alert.custom_data, dict):
+                raise ValueError('ApsAlert.custom_data must be a dict.')
+            for key, val in apns_payload_alert.custom_data.items():
+                result[key] = val
+        return cls.remove_null_values(result)
+
+    @classmethod
+    def encode_apns_hms_options(cls, apns_hms_options):
+        """
+        :param apns_hms_options:
+        """
+        if apns_hms_options is None:
+            return None
+        if not isinstance(apns_hms_options, _messages.APNsHMSOptions):
+            raise ValueError('Aps.alert must be a string or an instance of _messages.APNsHMSOptions class.')
+
+        result = {
+            'target_user_type': apns_hms_options.target_user_type,
+        }
+        return cls.remove_null_values(result)

+ 917 - 0
Service/VSeesHuaweiPushService/push_admin/_messages.py

@@ -0,0 +1,917 @@
+# -*-coding:utf-8-*-
+#
+# Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numbers
+import re
+import six
+
+
+class Message(object):
+    """A message that can be sent Huawei Cloud Messaging.
+
+    Args:
+        data: A string value.
+        notification: An instance of ``messaging.Notification`` (optional).
+        android: An instance of ``messaging.Android`` (optional).
+        apns: APSN related message definition
+        web_push: Web Push related message definition
+        token: token list, must be tuple (optional).
+        topic: message topic, must be string (optional).
+        condition: message condition, must be string (optional).
+    """
+    def __init__(self, data=None, notification=None, android=None, apns=None, web_push=None, token=None,
+                 topic=None, condition=None):
+        MessageValidator.check_message(data, notification, android, apns, web_push, token, topic, condition)
+        self.data = data
+        self.notification = notification
+        self.android = android
+        self.apns = apns
+        self.web_push = web_push
+        self.token = token
+        self.topic = topic
+        self.condition = condition
+
+
+class Notification(object):
+    """A notification that can be included in a message.
+
+    Args:
+        title: Title of the notification (optional).
+        body: Body of the notification (optional).
+    """
+    def __init__(self, title=None, body=None, image=None):
+        MessageValidator.check_notification(title, body, image)
+        self.title = title
+        self.body = body
+        self.image = image
+
+
+# ----------------------------------------------------------------------------------------------------------------------
+
+
+class APNsConfig(object):
+    """
+    Please refer to the Apple APNS API reference:
+    https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/\
+    CommunicatingwithAPNs.html
+    """
+    def __init__(self, headers=None, payload=None, apns_hms_options=None):
+        MessageValidator.check_apns_config(headers=headers, payload=payload, apns_hms_options=apns_hms_options)
+        self.headers = headers
+        self.payload = payload
+        self.apns_hms_options = apns_hms_options
+
+
+class APNsHeader(object):
+    """
+    authorization
+    apns-id
+    apns-expiration
+    apns-priority
+    apns-topic
+    apns-collapse-id
+    """
+    HEAD_AUTHORIZATION = "authorization"
+    HEAD_APNs_ID = "apns-id"
+    HEAD_APNs_EXPIRATION = "apns-expiration"
+    HEAD_APNs_PRIORITY = "apns-priority"
+    HEAD_APNs_TOPIC = "pns-topic"
+    HEAD_APNs_COLLAPSE_ID = "apns-collapse-id"
+
+
+class APNsPayload(object):
+    """
+     APNs payload definition
+    """
+    def __init__(self, aps, **kwargs):
+        MessageValidator.check_apns_payload(aps=aps)
+        self.aps = aps
+        self.custom_data = kwargs
+
+
+class APNsAps(object):
+    """
+    APNs aps definition: https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual\
+                        /RemoteNotificationsPG/PayloadKeyReference.html#//apple_ref/doc/uid/TP40008194-CH17-SW1
+
+    one sample is as follows:
+
+    {
+        "aps" : {
+            "alert" : {
+                "title" : "Game Request",
+                "body" : "Bob wants to play poker",
+                "action-loc-key" : "PLAY"
+                "loc-key" : "GAME_PLAY_REQUEST_FORMAT",
+                "loc-args" : [ "Jenna", "Frank"],
+                "content-available" : 1
+            },
+            "badge" : 5,
+            "sound" : "bingbong.aiff",
+        },
+        "acme1" : "bar",
+        "acme2" : [ "bang",  "whiz" ]
+    }
+    """
+    def __init__(self, alert=None, badge=None, sound=None, content_available=None, category=None,
+                 thread_id=None, mutable_content=None, custom_data=None):
+        MessageValidator.check_apns_payload_aps(alert=alert, badge=badge, sound=sound,
+                                                content_available=content_available, category=category,
+                                                thread_id=thread_id, mutable_content=mutable_content,
+                                                custom_data=custom_data)
+        self.alert = alert
+        self.badge = badge
+        self.sound = sound
+        self.content_available = content_available
+        self.category = category
+        self.thread_id = thread_id
+        self.mutable_content = mutable_content
+        self.custom_data = custom_data
+
+
+class APNsAlert(object):
+    """An alert that can be included in ``messaging.Aps``.
+
+    Args:
+
+    """
+
+    def __init__(self, title=None, body=None, loc_key=None, loc_args=None,
+                 title_loc_key=None, title_loc_args=None, action_loc_key=None, launch_image=None,
+                 custom_data=None):
+        MessageValidator.check_apns_payload_aps_alert(title=title, body=body, loc_key=loc_key, loc_args=loc_args,
+                                                      title_loc_key=title_loc_key, title_loc_args=title_loc_args,
+                                                      action_loc_key=action_loc_key, launch_image=launch_image,
+                                                      custom_data=custom_data)
+        self.title = title
+        self.body = body
+        self.loc_key = loc_key
+        self.loc_args = loc_args
+        self.title_loc_key = title_loc_key
+        self.title_loc_args = title_loc_args
+        self.action_loc_key = action_loc_key
+        self.launch_image = launch_image
+        self.custom_data = custom_data
+
+
+class APNsHMSOptions(object):
+    """Options for features provided by the FCM SDK for iOS.
+
+    Args:
+        target_user_type: Developer or Commercial enviroment
+    """
+    def __init__(self, target_user_type=None):
+        MessageValidator.check_apns_hms_options(target_user_type=target_user_type)
+        self.target_user_type = target_user_type
+
+
+# ----------------------------------------------------------------------------------------------------------------------
+
+
+class WebPushConfig(object):
+    """
+        Web push-specific options that can be included in a message.
+        For Web Push Specification Reference: https://tools.ietf.org/html/rfc8030#section-5
+        For mozilla implementation: https://developer.mozilla.org/en-US/docs/Web/API/notification
+    """
+    TTL_HEADER = "ttl"
+    URGENCY_HEADER = "urgency"
+    TOPIC_HEADER = "topic"
+
+    def __init__(self, headers=None, data=None, notification=None, hms_options=None):
+        """
+
+        :param headers: A dictionary of headers (optional). Refer `Web push Specification`_ for supported headers.
+        :param notification:  A ``messaging.WebPushNotification`` to be included in the message (optional).
+        :param hms_options:  A ``WebPushHMSOptions`` instance to be included in the message(optional).
+        """
+        MessageValidator.check_webpush_config(headers, data, notification, hms_options)
+        """ Refer to https://tools.ietf.org/html/rfc7240 """
+        self.headers = headers
+        """ message deliver to the end application directly """
+        self.data = data
+        """ Refer to  WebPushNotification """
+        self.notification = notification
+        """ Refer to WebPushHMSOptions"""
+        self.hms_options = hms_options
+
+
+class WebPushHeader(object):
+    """
+     Web Push Header, refer to: https://tools.ietf.org/html/rfc7240
+    """
+    def __init__(self, ttl=None, urgency=None, topic=None):
+        MessageValidator.check_webpush_header(ttl, urgency, topic)
+        self.ttl = ttl
+        self.urgency = urgency
+        self.topic = topic
+
+
+class WebPushNotification(object):
+    """
+     Web Push Notification
+    """
+    def __init__(self, title=None, body=None, icon=None, actions=None, badge=None, data=None, dir=None,
+                 image=None, lang=None, renotify=None, require_interaction=None, silent=None, tag=None,
+                 timestamp=None, vibrate=None):
+        MessageValidator.check_webpush_notification(title=title, body=body, icon=icon, actions=actions, badge=badge,
+                                                    data=data, dir=dir, image=image, lang=lang,
+                                                    renotify=renotify, require_interaction=require_interaction,
+                                                    silent=silent, tag=tag, timestamp=timestamp, vibrate=vibrate)
+        self.title = title
+        self.body = body
+        """ Refer to WebPushNotificationAction """
+        self.actions = actions
+        self.badge = badge
+        self.data = data
+        self.dir = dir
+        self.icon = icon
+        self.image = image
+        self.lang = lang
+        self.renotify = renotify
+        self.require_interaction = require_interaction
+        self.silent = silent
+        self.tag = tag
+        self.timestamp = timestamp
+        self.vibrate = vibrate
+
+
+class WebPushNotificationAction(object):
+    """
+    The action for web push notification
+    """
+    def __init__(self, action=None, title=None, icon=None):
+        """
+
+        :param action:
+        :param title:
+        :param icon:
+        """
+        MessageValidator.check_webpush_notification_action(action=action, title=title, icon=icon)
+        self.action = action
+        self.icon = icon
+        self.title = title
+
+
+class WebPushHMSOptions(object):
+    """
+    optional link option
+    """
+    def __init__(self, link=None):
+        MessageValidator.check_webpush_hms_options(link)
+        self.link = link
+
+
+# ----------------------------------------------------------------------------------------------------------------------
+
+
+class AndroidConfig(object):
+
+    HIGH_PRIORITY = "HIGH"
+    NORMAL_PRIORITY = "NORMAL"
+
+    """
+    Android-specific options that can be included in a message.
+    """
+    def __init__(self, collapse_key=None, urgency='NORMAL', ttl=None, bi_tag=None
+                 , fast_app_target=None, notification=None, data=None, category=None):
+        MessageValidator.check_android_config(collapse_key, urgency, ttl, bi_tag, fast_app_target
+                                              , notification, data)
+        self.collapse_key = collapse_key
+        self.urgency = urgency
+        self.ttl = ttl
+        self.bi_tag = bi_tag
+        self.fast_app_target = fast_app_target
+        self.notification = notification
+        self.data = data
+        self.category = category
+
+
+class AndroidNotification(object):
+
+    PRIORITY_LOW = "LOW"
+    PRIORITY_DEFAULT = "NORMAL"
+    PRIORITY_HIGH = "HIGH"
+    VISIBILITY_UNSPECIFIED = "VISIBILITY_UNSPECIFIED"
+    PRIVATE = "PRIVATE"
+    PUBLIC = "PUBLIC"
+    SECRET = "SECRET"
+
+    """
+    Android-specific notification parameters.
+    """
+
+    def __init__(self, title=None, body=None, icon=None, color=None, sound=None, default_sound=None, tag=None,
+                 click_action=None, body_loc_key=None, body_loc_args=None, title_loc_key=None,
+                 title_loc_args=None, multi_lang_key=None, channel_id=None, notify_summary=None, image=None,
+                 style=None, big_title=None, big_body=None, auto_clear=None, notify_id=None, group=None, badge=None,
+                 ticker=None, auto_cancel=None, when=None, importance=None, use_default_vibrate=True,
+                 use_default_light=True, vibrate_config=None, visibility=None, light_settings=None, foreground_show=False,
+                 buttons=None):
+
+        MessageValidator.check_android(title=title, body=body, icon=icon, color=color, sound=sound,
+                                       default_sound=default_sound, tag=tag, click_action=click_action,
+                                       body_loc_key=body_loc_key, body_loc_args=body_loc_args,
+                                       title_loc_key=title_loc_key, title_loc_args=title_loc_args,
+                                       multi_lang_key=multi_lang_key, channel_id=channel_id,
+                                       notify_summary=notify_summary,
+                                       image=image, style=style, big_title=big_title, big_body=big_body,
+                                       auto_clear=auto_clear, notify_id=notify_id,
+                                       group=group, badge=badge, ticker=ticker, auto_cancel=auto_cancel, when=when,
+                                       importance=importance,
+                                       use_default_vibrate=use_default_vibrate,
+                                       use_default_light=use_default_light, vibrate_config=vibrate_config,
+                                       visibility=visibility, light_settings=light_settings,
+                                       foreground_show=foreground_show)
+        self.title = title
+        self.body = body
+        self.icon = icon
+        self.color = color
+        self.sound = sound
+        self.default_sound = default_sound
+        self.tag = tag
+        self.click_action = click_action
+        self.body_loc_key = body_loc_key
+        self.body_loc_args = body_loc_args
+        self.title_loc_key = title_loc_key
+        self.title_loc_args = title_loc_args
+        self.multi_lang_key = multi_lang_key
+        self.channel_id = channel_id
+        self.notify_summary = notify_summary
+        self.image = image
+        self.style = style
+        self.big_title = big_title
+        self.big_body = big_body
+        self.auto_clear = auto_clear
+        self.notify_id = notify_id
+        self.group = group
+        self.badge = badge
+        self.ticker = ticker
+        self.auto_cancel = auto_cancel
+        self.when = when
+        self.importance = importance
+        self.use_default_vibrate = use_default_vibrate
+        self.use_default_light = use_default_light
+        self.vibrate_config = vibrate_config
+        self.visibility = visibility
+        self.light_settings = light_settings
+        self.foreground_show = foreground_show
+        self.buttons = buttons
+
+
+class AndroidClickAction(object):
+    """A ClickAction that can be included in a message.android.notification.
+
+    Args:
+        action_type: type of the android.notification (optional).
+        intent: intent of the android.notification (optional).
+        url: url of the android.notification (optional).
+        action: action definition for push message
+                1: to specific activity of application
+                2: specific URL
+                3: to specific application
+    """
+    def __init__(self, action_type=None, intent=None, action=None, url=None):
+        MessageValidator.check_click_action(action_type=action_type, intent=intent, action=action, url=url)
+        self.action_type = action_type
+        self.intent = intent
+        self.action = action
+        self.url = url
+
+class AndroidBadgeNotification(object):
+    """A BadgeNotification that can be included in a message.android.notification.
+
+    Args:
+        add_num: message number of badge notification in the android.notification (optional).
+        set_num: set the specific number of badge notification (optional).
+        clazz: message class of badge notification in the android.notification (optional).
+    """
+    def __init__(self, add_num=None, set_num=None, clazz=None):
+        MessageValidator.check_badge_notification(add_num=add_num, set_num=set_num, clazz=clazz)
+        self.add_num = add_num
+        self.set_num = set_num
+        self.clazz = clazz
+
+
+class AndroidLightSettings(object):
+    """
+        light_settings":{
+            "color":{
+                "alpha":0,
+                "red":0,
+                "green":1,
+                "blue":1
+            },
+            "light_on_duration":"3.5",
+            "light_off_duration":"5S"
+        }
+    """
+    def __init__(self, color=None, light_on_duration=None, light_off_duration=None):
+        MessageValidator.check_light_settings(color=color, light_on_duration=light_on_duration, light_off_duration=light_off_duration)
+        self.color = color
+        self.light_on_duration = light_on_duration
+        self.light_off_duration = light_off_duration
+
+
+class AndroidLightSettingsColor(object):
+    """
+        "color":{
+                "alpha":0,
+                "red":0,
+                "green":1,
+                "blue":1
+            }
+    """
+    def __init__(self, alpha=None, red=None, green=None, blue=None):
+        MessageValidator.check_light_settings_color(alpha=alpha, red=red, green=green, blue=blue)
+        self.alpha = alpha
+        self.red = red
+        self.green = green
+        self.blue = blue
+
+# --------------------------------------------------------------------------------------------------------------------
+
+
+class MessageValidator(object):
+    """
+        message validation utilities.
+        Methods provided in this class raise ValueErrors if any validations fail.
+    """
+    @classmethod
+    def check_https_url(cls, hint, value):
+        cls.check_string(hint, value)
+        if value is not None and not re.match(r"^https:/{2}\w.+$", value):
+            raise ValueError('{0} must be a valid https url.'.format(hint))
+
+    @classmethod
+    def check_string(cls, hint, value, non_empty=False):
+        """Checks if the given value is a string."""
+        if value is None:
+            return None
+        if not isinstance(value, six.string_types):
+            if non_empty:
+                raise ValueError('{0} must be a non-empty string.'.format(hint))
+            else:
+                raise ValueError('{0} must be a string.'.format(hint))
+        if non_empty and not value:
+            raise ValueError('{0} must be a non-empty string.'.format(hint))
+        return value
+
+    @classmethod
+    def assert_string_values(cls, hint, value, *args):
+        """
+        Check the class value should be an instance of string, and related values should be within *args
+        :param hint: prompt message
+        :param value: the real value
+        :param args: the specific value list
+        :return:
+        """
+        if value is None:
+            return None
+        if not isinstance(value, six.string_types):
+                raise ValueError('{0} must be a string.'.format(hint))
+        for v in args:
+            if value.__eq__(v):
+                return value
+
+        raise ValueError('{} must be a value within{}.'.format(hint, args))
+
+    @classmethod
+    def check_string_list(cls, label, value):
+        """Checks if the given value is a list comprised only of strings."""
+        if value is None or value == []:
+            return None
+        if not isinstance(value, list):
+            raise ValueError('{0} must be a list of strings.'.format(label))
+        non_str = [k for k in value if not isinstance(k, six.string_types)]
+        if non_str:
+            raise ValueError('{0} must not contain non-string values.'.format(label))
+        return value
+
+    @classmethod
+    def check_boolean(cls, hint, value):
+        """Checks if the given value is a string."""
+        if value is None:
+            return None
+        if not isinstance(value, bool):
+            raise ValueError('{0} must be a boolean.'.format(hint))
+        return value
+
+    @classmethod
+    def count_boolean(cls, *args):
+        count = 0
+        for v in args:
+            if v:
+                count += 1
+        return count
+
+    @classmethod
+    def check_not_all_none(cls, hint, *args):
+        total_size = len(args)
+        count = 0
+        for data in args:
+            if data is None:
+                count += 1
+        if total_size == count:
+            raise ValueError(hint)
+
+    @classmethod
+    def check_type(cls, class_obj, class_type, hint):
+        if (class_obj is not None) and (not isinstance(class_obj, class_type)):
+            raise ValueError(hint)
+
+    @classmethod
+    def check_type_list(cls, label, value, cls_type):
+        """Checks if the given value is a list comprised only of numbers."""
+        if value is None or value == []:
+            return None
+        if not isinstance(value, list):
+            raise ValueError('{0} must be a list of {1}.'.format(label, cls_type))
+        non_number = [k for k in value if not isinstance(k, cls_type)]
+        if non_number:
+            raise ValueError('{0} must not contain non-{1} values.'.format(label, cls_type))
+        return value
+
+    @classmethod
+    def check_number(cls, label, value):
+        if value is None:
+            return None
+        if not isinstance(value, numbers.Number):
+            raise ValueError('{0} must be a number.'.format(label))
+        return value
+
+    @classmethod
+    def check_number_span(cls, label, value, min, max):
+        if value is None:
+            return None
+        if not isinstance(value, numbers.Number):
+            raise ValueError('{0} must be a number.'.format(label))
+        if value < min or value > max:
+            raise ValueError('{0} must be within {1} to {2}.'.format(label, min, max))
+        return value
+
+    @classmethod
+    def assert_integer_values(cls, hint, value, *args):
+        """
+        Check the class value should be an instance of string, and related values should be within *args
+        :param hint: prompt message
+        :param value: the real value
+        :param args: the specific value list
+        :return:
+        """
+        if value is None:
+            return None
+        if not isinstance(value, six.integer_types):
+            raise ValueError('{0} must be a integer.'.format(hint))
+        for v in args:
+            if value == v:
+                return value
+
+        raise ValueError('{} must be a value within{}.'.format(hint, args))
+
+    @classmethod
+    def check_number_list(cls, label, value):
+        if value is None or value == []:
+            return None
+        if not isinstance(value, list):
+            raise ValueError('{0} must be a list of numbers.'.format(label))
+        non_number = [k for k in value if not isinstance(k, numbers.Number)]
+        if non_number:
+            raise ValueError('{0} must not contain non-number values.'.format(label))
+        return value
+
+    @classmethod
+    def check_string_dict(cls, label, value):
+        if value is None or value == {}:
+            return None
+        if not isinstance(value, dict):
+            raise ValueError('{0} must be a dictionary.'.format(label))
+        non_str = [k for k in value if not isinstance(k, six.string_types)]
+        if non_str:
+            raise ValueError('{0} must not contain non-string keys.'.format(label))
+        return value
+
+    # ------------------------------------------------------------------------------------------------------------------
+
+    @classmethod
+    def check_message(cls, data, notification, android, apns, web_push, token, topic, condition):
+        """
+        Check whether the message parameter is valid or not
+
+        :param data:
+        :param notification:
+        :param android:
+        :param apns:
+        :param web_push:
+        :param token:
+        :param topic:
+        :param condition:
+        :return:
+        """
+        # data must be string
+        cls.check_string(hint="Message.data", value=data)
+
+        # notification
+        if (notification is not None) and (not isinstance(notification, Notification)):
+            raise ValueError('notification must be an instance of Notification class')
+
+        # android / APNs / Web Push
+        # if notification message(data is None), one of android / APNs / Web Push must be present
+        if data is None:
+            cls.check_not_all_none('Message.data is None, one of Message.android/Message.apns/Message.webpush \
+            must be present', android, apns, web_push)
+
+        cls.check_type(android, AndroidConfig, 'android must be an instance of AndroidConfig class')
+        cls.check_type(apns, APNsConfig, 'apns must be an instance of APNsConfig class')
+        cls.check_type(web_push, WebPushConfig, 'web_push must be an instance of WebPushConfig class')
+
+        """token, topic, condition"""
+        # [token, topic, condition] only one not None
+        target_count = cls.count_boolean(token is not None, topic is not None, condition is not None)
+        if target_count != 1:
+            raise ValueError('Exactly one of token, topic or condition must be specified.')
+
+        # token must be tuple or list
+        if token is not None:
+            if not isinstance(token, tuple) and not isinstance(token, list):
+                raise ValueError('token must be a tuple or a list')
+            if len(token) > 1000:
+                raise ValueError('token must not contain more than 1000 tokens')
+
+        cls.check_string(hint="Message.topic", value=topic)
+        cls.check_string(hint="Message.condition", value=condition)
+
+    @classmethod
+    def check_notification(cls, title, body, image):
+        cls.check_string(hint="Notification.title", value=title)
+        cls.check_string(hint="Notification.body", value=body)
+        cls.check_https_url(hint="Notification.image", value=image)
+
+    @classmethod
+    def check_android_config(cls, collapse_key, urgency, ttl, bi_tag, fast_app_target, notification, data):
+        # collapse_key
+        cls.check_number('AndroidConfig.collapse_key', collapse_key)
+        # urgency
+        cls.assert_string_values("AndroidConfig.urgency", urgency, AndroidConfig.HIGH_PRIORITY,
+                                 AndroidConfig.NORMAL_PRIORITY)
+        # ttl
+        cls.check_string(hint="AndroidConfig.ttl", value=ttl)
+        # bi_tag
+        cls.check_string(hint="AndroidConfig.bi_tag", value=bi_tag)
+        # fast_app_target
+        cls.check_number_span("AndroidConfig.fast_app_target", fast_app_target, 1, 2)
+        # notification
+        cls.check_type(notification, AndroidNotification,
+                       hint='notification must be an instance of AndroidNotification')
+        # data
+        cls.check_string(hint="AndroidConfig.data", value=data)
+
+    @classmethod
+    def check_android(cls, title, body, icon, color, sound, default_sound, tag, click_action, body_loc_key, body_loc_args,
+                      title_loc_key, title_loc_args, multi_lang_key, channel_id, notify_summary, image,
+                      style, big_title, big_body, auto_clear, notify_id, group, badge,
+                      ticker, auto_cancel, when, importance,
+                      use_default_vibrate, use_default_light, vibrate_config, visibility, light_settings,
+                      foreground_show):
+        # title
+        cls.check_string(hint="AndroidNotification.title", value=title)
+        # body
+        cls.check_string(hint="AndroidNotification.body", value=body)
+        # icon
+        cls.check_string(hint="AndroidNotification.icon", value=icon)
+        # color
+        cls.check_string(hint="AndroidNotification.color", value=color)
+        # sound
+        cls.check_string(hint="AndroidNotification.sound", value=sound)
+        # default_sound
+        cls.check_boolean(hint="AndroidNotification.default_sound", value=default_sound)
+        # tag
+        cls.check_string(hint="AndroidNotification.tag", value=tag)
+        # click_action
+        cls.check_type(click_action, AndroidClickAction,
+                       hint='click_action must be an instance of AndroidClickAction')
+        # body_loc_key
+        cls.check_string(hint="AndroidNotification.body_loc_key", value=body_loc_key)
+        # body_loc_args
+        if (body_loc_args is not None) and (not isinstance(body_loc_args, tuple)) and (not isinstance(body_loc_args, list)):
+            raise ValueError('AndroidNotification.body_loc_args must be an instance of tuple or list')
+        # title_loc_key
+        cls.check_string(hint="AndroidNotification.title_loc_key", value=title_loc_key)
+        # title_loc_args
+        if (title_loc_args is not None) and (not isinstance(title_loc_args, tuple) and not isinstance(title_loc_args, list)):
+            raise ValueError('AndroidNotification.title_loc_args must be an instance of tuple or list')
+        # multi_lang_key
+        if multi_lang_key is not None:
+            if not isinstance(multi_lang_key, dict):
+                raise ValueError('AndroidNotification.multi_lang_key must be a dict.')
+        # channel_id
+        cls.check_string(hint="AndroidNotification.channel_id", value=channel_id)
+        # notify_summary
+        cls.check_string(hint="AndroidNotification.notify_summary", value=notify_summary)
+        #
+        # image
+        cls.check_https_url(hint="AndroidNotification.image", value=image)
+        # style
+        if style is not None:
+            if style not in [0, 1, 2]:
+                raise ValueError('AndroidNotification.style must in [0, 1, 2]')
+            # big_title, big_body
+            if style == 1:
+                if (big_title is None) or (not isinstance(big_title, str)):
+                    raise ValueError('AndroidNotification.big_title must be valid string when style is 1')
+                if (big_body is None) and (not isinstance(big_body, str)):
+                    raise ValueError('AndroidNotification.big_body must be valid string when style is 1')
+        # auto_clear
+        cls.check_number(label='AndroidNotification.auto_clear ', value=auto_clear)
+        # notify_id
+        cls.check_number(label='AndroidNotification.notify_id ', value=notify_id)
+        # group
+        cls.check_string(hint="AndroidNotification.group", value=group)
+        # badge
+        cls.check_type(badge, AndroidBadgeNotification, "badge should be an instance of AndroidBadgeNotification")
+        # ticker
+        cls.check_string(hint="AndroidNotification.ticker", value=ticker)
+        # auto_cancel
+        cls.check_boolean(hint="AndroidNotification.auto_cancel", value=auto_cancel)
+        # when
+        cls.check_string(hint="AndroidNotification.when", value=when)
+        # importance
+        cls.assert_string_values("AndroidNotification.importance", importance,
+                                 AndroidNotification.PRIORITY_DEFAULT,
+                                 AndroidNotification.PRIORITY_HIGH, AndroidNotification.PRIORITY_LOW)
+        # use_default_vibrate
+        cls.check_boolean(hint="AndroidNotification.use_default_vibrate", value=use_default_vibrate)
+        # use_default_light
+        cls.check_boolean(hint="AndroidNotification.use_default_light", value=use_default_light)
+        # vibrate_config
+        cls.check_string_list(label="AndroidNotification.vibrate_config", value=vibrate_config)
+        # visibility
+        cls.assert_string_values("AndroidNotification.visibility", visibility,
+                                 AndroidNotification.PRIVATE,
+                                 AndroidNotification.PUBLIC, AndroidNotification.SECRET,
+                                 AndroidNotification.VISIBILITY_UNSPECIFIED)
+        # light_settings
+        cls.check_type(light_settings, AndroidLightSettings, "light_settings should be an instance of AndroidLightSettings")
+        # foreground_show
+        cls.check_boolean(hint="AndroidNotification.foreground_show", value=foreground_show)
+
+    @classmethod
+    def check_badge_notification(cls, add_num, set_num, clazz):
+        # add_num must be int
+        cls.check_number_span(label="AndroidBadgeNotification.add_num", value=add_num, min=0, max=100)
+        # set_num must be int
+        cls.check_number_span(label="AndroidBadgeNotification.set_num", value=set_num, min=0, max=100)
+        # clazz
+        cls.check_string(hint="AndroidBadgeNotification.clazz", value=clazz)
+
+    @classmethod
+    def check_click_action(cls, action_type, intent, action, url):
+        # type must be in [1, 4]
+        if (action_type is None) or (action_type not in [1, 2, 3, 4]):
+            raise ValueError('ClickAction.type must be in [1, 2, 3, 4]')
+
+        # intent, if type is 1, intent or action must be present or both
+        if action_type == 1:
+            count = cls.count_boolean(isinstance(intent, str), isinstance(action, str))
+            if count <= 0:
+                raise ValueError('ClickAction.intent or ClickAction.action must be present or both when click_type is 1')
+
+        # url, if type is 2, url must
+        if action_type == 2:
+            if not isinstance(url, str):
+                raise ValueError('ClickAction.url must when ClickAction.type is 2')
+            if not url.upper().startswith('HTTPS'):
+                raise ValueError('ClickAction.url must be https prefix when ClickAction.type is 2')
+
+    @classmethod
+    def check_light_settings(cls, color, light_on_duration, light_off_duration):
+        cls.check_type(color, AndroidLightSettingsColor, "color must be an instance of AndroidLightSettingsColor")
+        cls.check_string(hint="AndroidLightSettings.light_on_duration", value=light_on_duration)
+        cls.check_string(hint="AndroidLightSettings.light_off_duration", value=light_off_duration)
+
+    @classmethod
+    def check_light_settings_color(cls, alpha, red, green, blue):
+        cls.check_number("AndroidLightSettingsColor.alpha", alpha)
+        cls.check_number("AndroidLightSettingsColor.red", red)
+        cls.check_number("AndroidLightSettingsColor.green", green)
+        cls.check_number("AndroidLightSettingsColor.blue", blue)
+
+    @classmethod
+    def check_webpush_config(cls, headers, data, notification, hms_options):
+        # headers
+        cls.check_type(headers, WebPushHeader, "headers must be an instance of WebPushHeader")
+        cls.check_string(hint="WebPushConfig.headers", value=data)
+        cls.check_type(notification, WebPushNotification, "notification must be an instance of WebPushNotification")
+        cls.check_type(hms_options, WebPushHMSOptions, "hms_options must be an instance of WebPushHMSOptions")
+
+    @classmethod
+    def check_webpush_header(cls, ttl=None, urgency=None, topic=None):
+        cls.check_string(hint="WebPushHeader.ttl", value=ttl)
+        cls.check_string(hint="WebPushHeader.urgency", value=urgency)
+        cls.check_string(hint="WebPushHeader.topic", value=topic)
+
+    @classmethod
+    def check_webpush_notification(cls, title=None, body=None, icon=None, actions=None, badge=None,
+                                   data=None, dir=None, image=None, lang=None, renotify=None,
+                                   require_interaction=None, silent=None, tag=None, timestamp=None, vibrate=None):
+        cls.check_string(hint="WebPushNotification.title", value=title)
+        cls.check_string(hint="WebPushNotification.body", value=body)
+        cls.check_string(hint="WebPushNotification.icon", value=icon)
+        cls.check_string(hint="WebPushNotification.data", value=data)
+        cls.check_type_list("WebPushNotificationAction.actions", actions, WebPushNotificationAction)
+        cls.check_string(hint="WebPushNotification.image", value=image)
+        cls.check_string(hint="WebPushNotification.lang", value=lang)
+        cls.check_string(hint="WebPushNotification.tag", value=tag)
+        cls.check_string(hint="WebPushNotification.badge", value=badge)
+        cls.assert_string_values("WebPushNotification.dir", dir, "auto", "ltr", "rtl")
+        cls.check_number_list("WebPushNotification.vibrate", vibrate)
+        cls.check_boolean("WebPushNotification.renotify", renotify)
+        cls.check_boolean("WebPushNotification.require_interaction", require_interaction)
+        cls.check_boolean("WebPushNotification.silent", silent)
+        cls.check_number("WebPushNotification.timestamp", timestamp)
+
+    @classmethod
+    def check_webpush_notification_action(cls, action=None, title=None, icon=None):
+        cls.check_string(hint="WebPushNotificationAction.action", value=action)
+        cls.check_string(hint="WebPushNotificationAction.title", value=title)
+        cls.check_string(hint="WebPushNotificationAction.icon", value=icon)
+
+    @classmethod
+    def check_webpush_hms_options(cls, link):
+        cls.check_string(hint="WebPushHMSOptions.link", value=link)
+
+    @classmethod
+    def check_apns_config(cls, headers=None, payload=None, apns_hms_options=None):
+        cls.check_string_dict("APNsConfig.headers", headers)
+        cls.check_type(payload, APNsPayload, "payload must be an instance of APNsPayload")
+        cls.check_type(apns_hms_options, APNsHMSOptions, "apns_hms_options must be an instance of APNsHMSOptions")
+
+    @classmethod
+    def check_apns_payload(cls, aps=None):
+        cls.check_type(aps, APNsAps, "aps must be an instance of APNsAps")
+        pass
+
+    @classmethod
+    def check_apns_payload_aps(cls, alert, badge, sound, content_available, category, thread_id, mutable_content,
+                               custom_data):
+        # alert: Dictionary or String
+        if alert is not None:
+            if not isinstance(alert, six.string_types):
+                cls.check_type(alert, APNsAlert, "alert must be an instance of String or APNsAlert class")
+        # badge: Number
+        cls.check_number("APNsAps.badge", badge)
+        # sound: String
+        cls.check_string("APNsAps.sound", sound)
+        # content_available: number
+        cls.check_number("APNsAps.content_available", content_available)
+        # category: String
+        cls.check_string("APNsAps.category", category)
+        # thread_id: String
+        cls.check_string("APNsAps.thread_id", thread_id)
+        # mutable_content
+        cls.check_boolean("APNsAps.mutable_content", mutable_content)
+        # custom_data
+        if custom_data is not None:
+            if not isinstance(custom_data, dict):
+                raise ValueError('APNsAps.custom_data must be a dict.')
+
+    @classmethod
+    def check_apns_payload_aps_alert(cls, title, body, loc_key, loc_args, title_loc_key, title_loc_args, action_loc_key,
+                                     launch_image, custom_data):
+        # title: String
+        cls.check_string("APNsAlert.title", title)
+        # body: String
+        cls.check_string("APNsAlert.body", body)
+        # loc_key: String
+        cls.check_string("APNsAlert.loc_key", loc_key)
+        # loc_args: Array of strings
+        cls.check_string_list("APNsAlert.loc_args", loc_args)
+        # title_loc_key: String or null
+        cls.check_string("APNsAlert.title_loc_key", title_loc_key)
+        # title_loc_args: Array of strings or null
+        cls.check_string_list("APNsAlert.title_loc_args", title_loc_args)
+        # action_loc_key: String or null
+        cls.check_string("APNsAlert.action_loc_key", action_loc_key)
+        # launch_image: String
+        cls.check_string("APNsAlert.launch_image", launch_image)
+        # custom_data
+        if custom_data is not None:
+            if not isinstance(custom_data, dict):
+                raise ValueError('APNsAlert.custom_data must be a dict.')
+
+    @classmethod
+    def check_apns_hms_options(cls, target_user_type):
+        cls.assert_integer_values("APNsHMSOptions.target_user_type", target_user_type, 1, 2, 3)

+ 224 - 0
Service/VSeesHuaweiPushService/push_admin/messaging.py

@@ -0,0 +1,224 @@
+# -*-coding:utf-8-*-
+#
+# Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from Service.VSeesHuaweiPushService.push_admin import _messages, _app
+from Service.VSeesHuaweiPushService import push_admin
+
+"""HUAWEI Cloud Messaging module."""
+
+""" General Data structure """
+Message = _messages.Message
+Notification = _messages.Notification
+
+""" Web Push related data structure """
+WebPushConfig = _messages.WebPushConfig
+WebPushHeader = _messages.WebPushHeader
+WebPushNotification = _messages.WebPushNotification
+WebPushNotificationAction = _messages.WebPushNotificationAction
+WebPushHMSOptions = _messages.WebPushHMSOptions
+
+""" Android Push related data structure """
+AndroidConfig = _messages.AndroidConfig
+AndroidNotification = _messages.AndroidNotification
+AndroidClickAction = _messages.AndroidClickAction
+AndroidBadgeNotification = _messages.AndroidBadgeNotification
+AndroidLightSettings = _messages.AndroidLightSettings
+AndroidLightSettingsColor = _messages.AndroidLightSettingsColor
+
+""" APNS Push related data structure"""
+APNsConfig = _messages.APNsConfig
+APNsHeader = _messages.APNsHeader
+APNsPayload = _messages.APNsPayload
+APNsAps = _messages.APNsAps
+APNsAlert = _messages.APNsAlert
+APNsHMSOptions = _messages.APNsHMSOptions
+
+"""Common exception definition"""
+ApiCallError = _app.ApiCallError
+
+
+def send_message(message, validate_only=False, app_id=None, verify_peer=False):
+    """
+        Sends the given message Huawei Cloud Messaging (HCM)
+        :param message: An instance of ``messaging.Message``.
+        :param validate_only: A boolean indicating whether to run the operation in dry run mode (optional).
+        :param app_id: app id parameters obtained by developer alliance applying for Push service (optional).
+        :param verify_peer: (optional) Either a boolean, in which case it controls whether we verify
+            the server's TLS certificate, or a string, in which case it must be a path
+            to a CA bundle to use. Defaults to ``True``.
+        :return: SendResponse
+        Raises:
+            ApiCallError: If an error occurs while sending the message to the HCM service.
+    """
+    try:
+        response = push_admin.get_app(app_id).send(message, validate_only, verify_peer=verify_peer)
+        return SendResponse(response)
+    except Exception as e:
+        raise ApiCallError(repr(e))
+
+
+def subscribe_topic(topic, token_list, app_id=None):
+    """
+    :param topic: The specific topic
+    :param token_list: The token list to be added
+    :param app_id: application ID
+    """
+    try:
+        response = push_admin.get_app(app_id).subscribe_topic(topic, token_list)
+        return TopicSubscribeResponse(response)
+    except Exception as e:
+        raise ApiCallError(repr(e))
+
+
+def unsubscribe_topic(topic, token_list, app_id=None):
+    """
+    :param topic: The specific topic
+    :param token_list: The token list to be deleted
+    :param app_id: application ID
+    """
+    try:
+        response = push_admin.get_app(app_id).unsubscribe_topic(topic, token_list)
+        return TopicSubscribeResponse(response)
+    except Exception as e:
+        raise ApiCallError(repr(e))
+
+
+def list_topics(token, app_id=None):
+    """
+    :param token: The token to be queried
+    :param app_id: application ID
+    """
+    try:
+        response = push_admin.get_app(app_id).query_subscribe_list(token)
+        return TopicQueryResponse(response)
+    except Exception as e:
+        raise ApiCallError(repr(e))
+
+
+class SendResponse(object):
+    """
+        The response received from an send request to the HCM API.
+        response: received http response body text from HCM.
+    """
+    def __init__(self, response=None):
+        try:
+            self._code = response['code']
+            self._msg = response['msg']
+            self._requestId = response['requestId']
+        except Exception as e:
+            raise ValueError(format(repr(e)))
+
+    @property
+    def code(self):
+        """errcode"""
+        return self._code
+
+    @property
+    def reason(self):
+        """the description of errcode"""
+        return self._msg
+
+    @property
+    def requestId(self):
+        """A message ID string that uniquely identifies the message."""
+        return self._requestId
+
+
+class BaseTopicResponse(object):
+    """
+    {
+       "msg": "Success",
+       "code": "80000000",
+       "requestId": "157466304904000004000701"
+     }
+    """
+    def __init__(self, json_rsp=None):
+        if json_rsp is None:
+            self._msg = ""
+            self._code = ""
+            self._requestId = ""
+        else:
+            self._msg = json_rsp['msg']
+            self._code = json_rsp['code']
+            self._requestId = json_rsp['requestId']
+
+    @property
+    def msg(self):
+        return self._msg
+
+    @property
+    def code(self):
+        return self._code
+
+    @property
+    def requestId(self):
+        return self._requestId
+
+
+class TopicSubscribeResponse(BaseTopicResponse):
+    """
+     {
+       "msg": "Success",
+       "code": "80000000",
+       "requestId": "157466304904000004000701",
+       "successCount": 2,
+       "failureCount": 0,
+       "errors": []
+     }
+    """
+    def __init__(self, json_rsp=None):
+        super(TopicSubscribeResponse, self).__init__(json_rsp=json_rsp)
+        if json_rsp is None:
+            self._successCount = 0
+            self._failureCount = 0
+            self._errors = []
+        else:
+            self._successCount = json_rsp['successCount']
+            self._failureCount = json_rsp['failureCount']
+            self._errors = json_rsp['errors']
+
+    @property
+    def successCount(self):
+        return self._successCount
+
+    @property
+    def failureCount(self):
+        return self._failureCount
+
+    @property
+    def errors(self):
+        return self._errors
+
+
+class TopicQueryResponse(BaseTopicResponse):
+    """
+         {
+           "msg": "success",
+           "code": "80000000",
+           "requestId": "157466350121600008000701",
+           "topics": [
+                       { "name": "sports",
+                         "addDate": "2019-11-25"
+                         } ]
+         }
+    """
+    def __init__(self, json_rsp=None):
+        super(TopicQueryResponse, self).__init__(json_rsp)
+        self._topics = json_rsp['topics']
+
+    @property
+    def topics(self):
+        return self._topics