在 django 中使用 firebase 傳送通知

行行出bug發表於2018-09-10

應用描述: 嚮應用傳送通知的案例有很多:微信發表朋友圈後,有人回覆你,微信會有通知。通知中能帶一些資料,讓你知道微信中有哪些和你有關的事情發生。要實現通知首先需要了解通知的整個過程(這裡介紹手機的通知)。

  • 使用者在移動端的某些操作會在服務端觸發一些事情,服務端通過一定的方式給對應的移動端傳送訊號;
  • 移動端的應用程式實現了手機接收通知的介面的,能收到這個訊號,並獲取到資料;
  • 移動端應用程式直接解析資料或是去服務端拉取對應的詳情資料後再解析資料;
  • 移動端應用程式根據資料設定錨點,並展示通知。這樣點選通知獲取詳情後,能跳轉到應用程式的對應頁面;

這個過程中,移動端接收通知是被動的(如果是主動,移動端需要以心跳檢測的方式一直向伺服器傳送請求,這對伺服器的壓力太大);要達到被動接受效果需要藉助三方平臺(firebase)來完成這個事情。 所以在整個通知流程中,服務端需要做的是:

  • 在業務中激發對應的訊號,並將訊號需要的資料準備好

為了讓django應用程式對訊息推送'無感知',可以採用django訊號來完成,即:應用程式儲存某些資料的時候,框架會將save訊號傳送到對應的處理程式,這個處理程式中進行訊息推送處理

  • 呼叫三方平臺訊息推送介面,使之將資料傳送到對應的移動端

將三方平臺的呼叫繼整合到自己的程式中

  • 提供移動端程式呼叫介面,使之能拿到詳細的資料

服務端提供訊息詳情獲取介面,供移動端呼叫


相關元件介紹

1. firebase 通知功能介紹

Firebase 雲資訊傳遞 FCM 是一種跨平臺訊息傳遞解決方案,可供您免費、可靠地傳遞訊息 使用 FCM,您可以通知客戶端應用存在可同步的新電子郵件或其他資料。您可以傳送通知訊息以再次吸引使用者並留住他們。在即時通訊等使用情形中,一條訊息可將最多 4KB 的有效負載傳送至客戶端應用。

image
fcm訊息分兩種,這兩種使用時有一定的差異。移動端的應用程式在後臺執行時,如果伺服器使用的是Admin SDK,移動端可能收不到以通知形式傳送的訊息
image

伺服器要傳送訊息有兩種方式:

  • 直接呼叫官方提供 Admin SDK
  • 另一種是使用原生的協議,傳送指定格式的資料,這種方式需要自己處理髮送異常,實現重試機制。 使用官方SDK的,官方提供了對應語言的原始碼和呼叫示例,可以直接拿過來使用。
    image

2. django 訊號機制

Django Signals的機制是一種觀察者模式,又叫釋出-訂閱(Publish/Subscribe) 。當發生一些動作的時候,發出訊號,然後監聽了這個訊號的函式就會執行。 通俗來講,就是一些動作發生的時候,訊號允許特定的傳送者去提醒一些接受者。用於在框架執行操作時解耦。 特別是使用django內建訊號的時候,你執行完某個操作,不需要去手動呼叫訊號接收處理函式,這在一定的程度上實現了程式的'無感知'。傳送通知時,用這個方式就很合適:業務層做完對應的業務,儲存資料到通知表,儲存後觸發訊號,框架去呼叫對應的處理函式(將通知傳送到移動端的函式),業務邏輯和移動端處理函式不用耦合在一起。

image


3. django 非同步

傳送訊號因要呼叫三方平臺,會比較耗時,如果讓業務程式執行後,使用處理業務程式的執行緒繼續處理通知的話,會大大的延長業務程式處理的時間,這會降低伺服器的處理效率。這裡可以新開執行緒來單獨處理通知。讓業務程式直接返回(業務處理程式不需要管通知處理的結果)。在django中要用額外的執行緒來處理這些,最好是使用celery,也可以自己手動新建執行緒來處理。

import threading

def post_save_callback(sender, **kwargs):
    target = threading.Thread(
        target=notice_handler,
        kwargs=kwargs,
    )
    target.start()
複製程式碼

設計思路和程式碼示例

1. 整合 FCM Admin SDK

這裡介紹下python整合的示例[官方文件]:

  • 新增SDK
sudo pip install firebase-admin
複製程式碼

找到對應應用的服務賬號設定

image

找到對應的語言,生成私鑰(這個必要暴露出來!):yourPrivateKey.json

image

  • 初始化SDK
class MessageHandler(object):
    _instance_lock = threading.Lock()

    def __init__(self):
        if not hasattr(self, 'app'):
            with MessageHandler._instance_lock:
                if not hasattr(self, 'app'):
                    self.cred = credentials.Certificate('yourPath/yourPrivateKey.json')
                    self.default_app = firebase_admin.initialize_app(self.cred)
                    self.app = self.default_app

    def __new__(cls, *args, **kwargs):
        if not hasattr(MessageHandler, "_instance"):
            with MessageHandler._instance_lock:
                if not hasattr(MessageHandler, "_instance"):
                    MessageHandler._instance = object.__new__(cls)
        return MessageHandler._instance

    def get_app(self):
        return self.app


_message_handler = MessageHandler()


if '__name__' == 'main':
    print('register success!')

複製程式碼

直接執行這個檔案,能執行成功,證明配置正確 配置正確後可以在專案啟動的時候就完成物件的註冊:在 admin 模組的類中新增ready,並將物件 import 過來(後續的訊號註冊也需要這樣做)

from django.apps import AppConfig


class TestConfig(AppConfig):
    name = 'test'
    verbose_name = 'Test'

    def ready(self):
        import your MessageHandler file name

複製程式碼

2. 編寫訊息傳送函式來呼叫 Admin SDK API

官方有對應示例, 可以在此基礎上調整後直接使用

# Copyright 2018 Google Inc. 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 __future__ import print_function

import datetime

from firebase_admin import messaging


def send_to_token():
    # [START send_to_token]
    # This registration token comes from the client FCM SDKs.
    registration_token = 'YOUR_REGISTRATION_TOKEN'

    # See documentation on defining a message payload.
    message = messaging.Message(
        data={
            'score': '850',
            'time': '2:45',
        },
        token=registration_token,
    )

    # Send a message to the device corresponding to the provided
    # registration token.
    response = messaging.send(message)
    # Response is a message ID string.
    print('Successfully sent message:', response)
    # [END send_to_token]


def send_to_topic():
    # [START send_to_topic]
    # The topic name can be optionally prefixed with "/topics/".
    topic = 'highScores'

    # See documentation on defining a message payload.
    message = messaging.Message(
        data={
            'score': '850',
            'time': '2:45',
        },
        topic=topic,
    )

    # Send a message to the devices subscribed to the provided topic.
    response = messaging.send(message)
    # Response is a message ID string.
    print('Successfully sent message:', response)
    # [END send_to_topic]


def send_to_condition():
    # [START send_to_condition]
    # Define a condition which will send to devices which are subscribed
    # to either the Google stock or the tech industry topics.
    condition = "'stock-GOOG' in topics || 'industry-tech' in topics"

    # See documentation on defining a message payload.
    message = messaging.Message(
        notification=messaging.Notification(
            title='$GOOG up 1.43% on the day',
            body='$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.',
        ),
        condition=condition,
    )

    # Send a message to devices subscribed to the combination of topics
    # specified by the provided condition.
    response = messaging.send(message)
    # Response is a message ID string.
    print('Successfully sent message:', response)
    # [END send_to_condition]


def send_dry_run():
    message = messaging.Message(
        data={
            'score': '850',
            'time': '2:45',
        },
        token='token',
    )

    # [START send_dry_run]
    # Send a message in the dry run mode.
    response = messaging.send(message, dry_run=True)
    # Response is a message ID string.
    print('Dry run successful:', response)
    # [END send_dry_run]


def android_message():
    # [START android_message]
    message = messaging.Message(
        android=messaging.AndroidConfig(
            ttl=datetime.timedelta(seconds=3600),
            priority='normal',
            notification=messaging.AndroidNotification(
                title='$GOOG up 1.43% on the day',
                body='$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.',
                icon='stock_ticker_update',
                color='#f45342'
            ),
        ),
        topic='industry-tech',
    )
    # [END android_message]
    return message


def apns_message():
    # [START apns_message]
    message = messaging.Message(
        apns=messaging.APNSConfig(
            headers={'apns-priority': '10'},
            payload=messaging.APNSPayload(
                aps=messaging.Aps(
                    alert=messaging.ApsAlert(
                        title='$GOOG up 1.43% on the day',
                        body='$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.',
                    ),
                    badge=42,
                ),
            ),
        ),
        topic='industry-tech',
    )
    # [END apns_message]
    return message


def webpush_message():
    # [START webpush_message]
    message = messaging.Message(
        webpush=messaging.WebpushConfig(
            notification=messaging.WebpushNotification(
                title='$GOOG up 1.43% on the day',
                body='$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.',
                icon='https://my-server/icon.png',
            ),
        ),
        topic='industry-tech',
    )
    # [END webpush_message]
    return message


def all_platforms_message():
    # [START multi_platforms_message]
    message = messaging.Message(
        notification=messaging.Notification(
            title='$GOOG up 1.43% on the day',
            body='$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.',
        ),
        android=messaging.AndroidConfig(
            ttl=datetime.timedelta(seconds=3600),
            priority='normal',
            notification=messaging.AndroidNotification(
                icon='stock_ticker_update',
                color='#f45342'
            ),
        ),
        apns=messaging.APNSConfig(
            payload=messaging.APNSPayload(
                aps=messaging.Aps(badge=42),
            ),
        ),
        topic='industry-tech',
    )
    # [END multi_platforms_message]
    return message


def subscribe_to_topic():
    topic = 'highScores'
    # [START subscribe]
    # These registration tokens come from the client FCM SDKs.
    registration_tokens = [
        'YOUR_REGISTRATION_TOKEN_1',
        # ...
        'YOUR_REGISTRATION_TOKEN_n',
    ]

    # Subscribe the devices corresponding to the registration tokens to the
    # topic.
    response = messaging.subscribe_to_topic(registration_tokens, topic)
    # See the TopicManagementResponse reference documentation
    # for the contents of response.
    print(response.success_count, 'tokens were subscribed successfully')
    # [END subscribe]


def unsubscribe_from_topic():
    topic = 'highScores'
    # [START unsubscribe]
    # These registration tokens come from the client FCM SDKs.
    registration_tokens = [
        'YOUR_REGISTRATION_TOKEN_1',
        # ...
        'YOUR_REGISTRATION_TOKEN_n',
    ]

    # Unubscribe the devices corresponding to the registration tokens from the
    # topic.
    response = messaging.unsubscribe_from_topic(registration_tokens, topic)
    # See the TopicManagementResponse reference documentation
    # for the contents of response.
    print(response.success_count, 'tokens were unsubscribed successfully')
    # [END unsubscribe]
複製程式碼

拿過來後就可以直接測試傳送訊息了,測試通過再往後面走(如何讓移動端接收到firebase的資訊的請參看對應文件)

3. 編寫訊號處理函式來呼叫訊號傳送函式

def send_to_token(notice):
    data = None  # your message data
    token = None  # 移動端 token
    return msg_handler.send_to_token(
        token,
        data,
    )
    

def notice_handler(**kwargs):
    """notificaton demo"""
    notice = kwargs.get('instance', None)
    send_to_token(notice)
複製程式碼

4. 用多執行緒方式註冊訊號處理函式,使之能'感知'到對應的業務操作

from django.db.models.signals import post_save


def post_save_callback(sender, **kwargs):
    target = threading.Thread(
        target=notice_handler,
        kwargs=kwargs,
    )
    target.start()
    
    
post_save.connect(
    post_save_callback,  # 指定回撥函式
    dispatch_uid='xxx',  # 限制訊息只傳送一次
    sender=NoticeModelClassName,  # 限制只接收和通知有關的訊息
)
複製程式碼

將以上程式碼組織下,一個簡陋版服務端訊息處理功能就完成了

相關文章