Browse Source

华为微瞳推送

locky 7 months ago
parent
commit
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.OCIObjectStorage import OCIObjectStorage
 from Object.UidTokenObject import UidTokenObject
+from Object.enums.ConstantEnum import ConstantEnum
 from Object.enums.EventTypeEnum import EventTypeEnumObj
 from Object.utils import LocalDateTimeUtil
 from Service.CommonService import CommonService
@@ -37,6 +38,8 @@ from Service.HuaweiPushService.HuaweiPushService import HuaweiPushObject
 from Service.PushService import PushObject
 from django.db import close_old_connections
 
+from Service.VSeesHuaweiPushService.VseesHuaweiPushObject import VseesHuaweiPushObject
+
 LOGGING = logging.getLogger('info')
 TIME_LOGGER = logging.getLogger('time')
 ERROR_INFO_LOGGER = logging.getLogger('error_info')
@@ -450,8 +453,12 @@ class DevicePushService:
                         push_result = PushObject.android_jpush(**kwargs)
 
                 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
                     if kwargs['event_type'] in EventTypeEnumObj.DATA_PUSH_EVENT_TYPE_LIST.value:
                         push_channel = 'push_to_talk'
@@ -791,11 +798,16 @@ class DevicePushService:
                 push_result = PushObject.android_fcm_push_v1(
                     uid, appBundleId, token_val, n_time, event_type, msg_title, msg_text, uid, channel, image_url)
             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))
         except Exception as e:
             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 Service.HuaweiPushService import push_admin
 from Service.HuaweiPushService.push_admin import messaging
-from AnsjerPush.config import HUAWEI_CONFIG
+
 
 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()
 
     def init_app(self):
@@ -37,11 +37,8 @@ class HuaweiPushObject:
         """
         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,
                                                 event_type, n_time, token_val, channel)
@@ -107,7 +104,7 @@ class HuaweiPushObject:
             assert (response.code == '80000000')
             return True
         except Exception as e:
-            LOGGER.info('华为通知推送异常: {}'.format(repr(e)))
+            LOGGER.error('uid:{}, 华为通知推送异常: {}'.format(uid ,repr(e)))
             return False
 
     @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