إرسال الإشعارات عبر Cloud Functions وبدون سيرفر خارجي

في هذا الشرح سنقوم بطرح كيفية إرسال الإشعارات الى المستخدمين باستخدام Firebase Cloud Functions بدون أي سيرفر خارجي

قبل البدء

في هذا الدرس سنشرح بعض الأمور المتقدمة ويجب عليك معرفة آلية إرسال الإشعارات عن طريق Firebase Cloud Messaging FCM ,ولهذا أنصحك بشدة في هذين المقالين من موقع هينديوير (وخاصة المقال الأول)

الجزء الأول , الجزء الثاني

ماهي Firebase Cloud Functions؟

هي خدمة قدمتها Google منذ بضعة أشهر ومازالت في المرحلة التجريبية BETA ,تساعدك Cloud Functions بتنفيذ أمور معينة عند حدوث أمر معين (عند الكتابة في قاعدة البيانات Firebase Realtime Database) . بعض الأمثلة:

  • إرسال الإشعارات
  • تعديل بعض النصوص على سبيل المثال (تغيير كلمة “Bad” الى “Not Good” بشكل أوتوماتيكي وبدون أي تدخل منك)
  • توليد الصور المصغرة “Thumbnails” بمساعدة Firebase Storage

والكثير من الأشياء الأخرى(ألقِ نظرة على الروابط في أسفل المقال)

نأتي الى الأمر المهم وهو كيف سنبدأ  بتجهيز بيئة العمل و نبدأ بإرسال الإشعارات .

الFirebase Cloud Functions مبنية على لغة Javascript وعلى Framework من نوع NodeJS

ولهذا يجب علينا أن نقوم بتحميل NodeJS من هذا الرابط ,بعد تثبيته نقوم بفتح CMD او Terminal ونكتب الأمر التالي لتثبيت أدوات Firebase

npm install -g firebase-tools

عند نجاح التثبيت ستظهر بهذا الشكل

ثم نكتب الأمر لتسجيل الدخول بحساب Google واختيار مشروع Firebase

firebase login

بعد ذلك ستُفتح صفحة ويب وتطلب منك تسجيل الدخول

اختر Allow

تمت عملية تسجيل الدخول

نعود الى CMD ونقوم بإنشاء مجلد جديد وثم غير مسار CMD الى هذا المجلد (او إذا كنت على ويندوز فقم بفتح المجلد واضغط على زر Shift + زر الفأرة الأيمن واختر “Open Command Window Here” )

ثم اكتب الأمر لبدء تجهيز مشروع Cloud Functions

firebase init functions

الآن سيتم عرض كافة مشاريع Firebase الموجودة في حسابك,قم باختيار المشروع الذي تريد

ثم اختر Y

تم تجهيز الملفات

الآن اذا ذهبنا الى المجلد سنجد به بعض الملفات

نتوجه الى مجلد functions وسنجد داخله ملف index.js هذا هو الملف الذي سنقوم بكتابة Cloud Functions بداخله

قم بفتحه باستخدام أي محرر أكواد,سأستخدم VSCode يمكنك تحميله من هنا

سنجد هذه الأكواد ,نقوم بمسحها

سنقوم في مثالنا هذا إرسال إشعار للمستخدم عندما يقوم أحد بمتابعته(بنفس فكرة موقع تويتر) وسنعرض إسم الشخص الذي قام بمتابعته بالإضافة الى صورته في الإشعار

ولنفترض أنه لديك مثل هذا الترتيب في قاعدة البيانات Realtime Database

جدول users يحتوي على جميع المستخدمين ونسمي كل مستخدم بناء على UID الخاص بFirebase Auth .

وداخل كل مستخدم نضع التالي:

  • notificationTokens (راجع الدروس من مدونة هينديوير)
  • photo رابط صورة المستخدم
  • userName اسم المستخدم

سنقوم بإنشاء جدول نسميه followers والذي سيحتوي على الأشخاص الذين قاموا بمتابعة هذا المستخدم

حان وقت العمل , فلنكتب بعض الأكواد 😀

ولكن قبل هذا سأذكرك بأن Firebase Cloud Fucntions تعمل على مبدأ تنفيذ أمر معين عند حدوث فعل(كتابة على Firebase Realtime Database مثلاً)

//Cloud Functions Modules
const functions = require('firebase-functions');
//Firebase Admin SDK Modules (it will send the Notifications to the user)
const admin = require('firebase-admin');
//init Admin SDK
admin.initializeApp(functions.config().firebase);

السطر الثاني نقوم بتعريف او عمل import لمكتبة Firebase Cloud Functions

السطر الرابع نقوم بتعريف مكتبة Firebase Admin وهي المسؤولة عن إرسال الإشعارات والكتابة في قاعدة البيانات ثم نقوم بتهيئة مكتبة Admin في السطر الأخير

exports.sendNotificationOnNewFollow = functions.database.ref('/followers/{userId}/{followerId}/').onWrite(event => {
}

بعد ذلك نقوم بتعريف Cloud Function عبر exports.sendNotificationOnNewFollow 

(sendNotificationOnNewFollow هو اسم الFunction يمكنك تسميته كما تشاء) ونجعلها تستمع الى الأحداث في جدول followers

مايكتب بين هذين القوسين{} عند تعريف Cloud Function يسمى Wildcard وببساطة يعني أننا نريد الإستماع الى الأحداث داخل جدول followers داخل userId داخل followerId ,كما أنها أيضاً تعيد لنا قيمة الحدث (كuserId و followerId)

وقد يحتوي على أي قيمة 

كما أنه يمكنك تسميتهم بأي إسم تريد

الصورة التالية ستوضح لك الفكرة

.onWrite أي أنه عندما يتم الكتابة وهي تعيد لنا حدث event والذي يحتوي على الأمور التى كتبت او تغيرت في Realtime Database

const userId = event.params.userId;
const followerId = event.params.followerId

هنا قمنا بالحصول على userId و followerId عن طريق event.params

يجب عليك أن تكتب نفس الأسماء التى في البارامترز

سنقوم بكتابة هذا السطر

 if (!event.data.exists()) {
    return;
  }

وهذا يعني أنه اذا كانت لاتتوفر بيانات(تم عمل متابعة ثم الغاؤها فوراً) عندها قم بعمل return ولا تنفذ أي شيئ آخر

ثم نقوم بتعريف ميثود getDeviceTokensPromise مهمتها جلب الnotificationTokens الخاصة بالمستخدم الذي تمت متابعته(“Ahmad”)

 const getDeviceTokensPromise = admin.database().ref(`users/${userId}/notificationTokens/`).once('value');

لاحظ أنه تم استخدام userId الذي عرفناه سابقاً

ثم بنفس الطريقة نعرف ميثود أخرى تقوم بجلب اسم المستخدم وصورته

const getFollowerInfo = admin.database().ref(`users/${followerId}/`).once('value');

هذه المرة قمنا بجلب البيانات الخاصة بناءً على followerId وهو (“Sobhe”)

الآن سنستدعي هذه الميثودز

في Cloud Functions يجب علينا دائما أن نعود ب Promise وهو الذي سيقول ل Cloud Functions أنه قد انتهى من التنفيذ

 return Promise.all([getDeviceTokensPromise, getFollowerInfo]).then(results => {
    const tokensSnapshot = results[0];
    const followerSnapshot = results[1];
 }

داخل Promise نعطيه الميثودز الذي أنشأناها ونضعها داخل مصفوفة Array.

.then هذه تعني عندما ينتهي من تنفيذ الميثوذز وهي تعود لنا ب Snapshot أسميناها results 

وبما أننا قد نفذنا أكثر من ميثود فقمنا بتعريف كل Snapshot على حدى وأعطيناها المكان من Array

ثم سنتأكد من أنه يوجد tokens أم لا عبر السطر

    if (!tokensSnapshot.hasChildren()) {
      return console.log('There are no notification tokens to send to.');
    }

واذا لم يوجد سيقوم بطبع رسالة وسيتجاهل ماتبقى من الكود

بعد ذلك نقوم بأخذ userName و photo من followerSnapshot ونقوم بطباعتهم في log

const followerName = followerSnapshot.val().userName;
const followerPhoto = followerSnapshot.val().photo;
console.log('Follower Name is: ', followerName);
console.log('Follower Photo is: ', followerPhoto);

ثم نقوم بتعريف Payload وهو الإشعار  الذي سنستقبله في تطبيق Android

وهو يحتوي على:

  • title عنوان الإشعار
  • body مانريد كتابته داخل الإشعار(التفاصيل)
  • imgUrl صورة المستخدم (“Sobhe”)
// Notification details.
    const payload = {
      data: {
        title: 'you have a New Follower',
        body: `${followerName} has followed You.`,
        imgUrl:  `${followerPhoto}`
      }
    };

ثم نقوم بأخذ notificationTokens من tokensSnapshot وبما أنه قد يحتوي على أكثر من عنصر قمنا بوضعهم في Array

const tokens = Object.keys(tokensSnapshot.val());

أخيراً نقوم بإرسال الإشعار عبر messaging وهي تأخذ 2 بارامتر tokens و payload

    // Send notifications to all tokens.
    return admin.messaging().sendToDevice(tokens, payload).then(response => {
})

وتعود لنا ب response 

ثم نقوم بتعريف مصفوفة Array فارغة,سنملؤها لاحقا بNotification Tokens الغير صالحة لنقوم بحذفهم

const tokensToRemove = [];

وأخيراً نقوم بكتابة هذه الأكواد

response.results.forEach((result, index) => {
        const error = result.error;
        if (error) {
          console.error('Failure sending notification to', tokens[index], error);
          // Cleanup the tokens who are not registered anymore.
          if (error.code === 'messaging/invalid-registration-token' ||
            error.code === 'messaging/registration-token-not-registered') {
            tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
          }
        }
      });
      return Promise.all(tokensToRemove);

إذا كان هنالك خطأ سنقوم بطباعته في Log ثم نعود ب Promise لحذف الTokens الغير صالحة في حال وجودهم

ليصبح الكود الكامل كالتالي

//Cloud Functions Modules
const functions = require('firebase-functions');
//Firebase Admin SDK Modules (it will send the Notifications to the user)
const admin = require('firebase-admin');
//init Admin SDK
admin.initializeApp(functions.config().firebase);

exports.sendNotificationOnNewFollow = functions.database.ref('/followers/{userId}/{followerId}/').onWrite(event => {
  const userId = event.params.userId;
  const followerId = event.params.followerId

  // If un-follow we exit the function.
  if (!event.data.exists()) {
    return;
  }

 
  // Get the list of device notification tokens.
  const getDeviceTokensPromise = admin.database().ref(`users/${userId}/notificationTokens/`).once('value');
  // Get the follower Info.
  const getFollowerInfo = admin.database().ref(`users/${followerId}/`).once('value');
  
//Execute the Functions
  return Promise.all([getDeviceTokensPromise, getFollowerInfo]).then(results => {
    const tokensSnapshot = results[0];
    const followerSnapshot = results[1];
  


    // Check if there are any device tokens.
    if (!tokensSnapshot.hasChildren()) {
      return console.log('There are no notification tokens to send to.');
    }

  const followerName = followerSnapshot.val().userName;
  const followerPhoto = followerSnapshot.val().photo;

    console.log('Follower Name is: ', followerName);
    console.log('Follower Photo is: ', followerPhoto);
    

    // Notification details.
    const payload = {
      data: {
        title: 'you have a New Follower',
        body: `${followerName} has followed You.`,
        imgUrl:  `${followerPhoto}`
      }
    };



    // Listing all tokens.
    const tokens = Object.keys(tokensSnapshot.val());

    // Send notifications to all tokens.
    return admin.messaging().sendToDevice(tokens, payload).then(response => {
      // For each message check if there was an error.
      const tokensToRemove = [];
      response.results.forEach((result, index) => {
        const error = result.error;
        if (error) {
          console.error('Failure sending notification to', tokens[index], error);
          // Cleanup the tokens who are not registered anymore.
          if (error.code === 'messaging/invalid-registration-token' ||
            error.code === 'messaging/registration-token-not-registered') {
            tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
          }
        }
      });
      return Promise.all(tokensToRemove);
    });
  });
});

نقوم بحفظ الملف Ctrl + S ونعود مرة أخرى الى CMD ونتوجه الى مجلد المشروع

ونكتب الأمر

firebase deploy --only functions

سيتم بدء رفع الملفات الى Firebase وقد تأخذ العملية بعض الوقت

عند الإنتهاء نذهب الى Firebase Console الى Functions وستجد ظهور Cloud Function الذي أنشأتها

سنتوجه الآن الى Android Studio بشكل سريع لترى كيف يتم تنفيذ الإشعار (يمكنك تحميل السورس كود وتعديله كما تشاء)

نقوم بإنشاء كلاس جديد نسميه MyFCMService والذي سيقوم بتلقى الإشعارات من Cloud Functions ونجعله extends FirebaseMessaginService ولاننسَ أن نقوم بتشغيله من MainActivity

ثم نقوم بعمل Override لميثود onMessageReceived والتى تعود لنا ب remoteMessage

ثم نقوم بتعريف title و body و imgUrl ونلاحظ أنه يجب علينا كتابة نفس الإسم المكتوب في Payload الذي كتبناه في Cloud Functions

ثم استدعينا ميثود سنقوم بإنشاءها وهي sendNotification وتأخذ المتغيرات الثلاثة ك بارامترز

ثم نقوم بتعريف هذه الميثود,وهي ميثود بسيطة تقوم بأخذ البارامترز وعرضهم في Notification

ونلاحظ أنه قد وضعنا صورة الشخص عبر setLargeIcon وقمنا باستدعاء ميثود getProfilePhotoAsBitmap
والتي تأخذ imgUrl ك بارامتر

    private void sendNotification(String title, String messageBody, String imgUrl) {

        Intent intent = new Intent(this, MyFCMService.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* FRequest code */, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(android.R.drawable.star_on)
                .setLargeIcon(getProfilePhotoAsBitmap(imgUrl))
                .setContentTitle(title)
                .setContentText(messageBody)
                .setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
}

أخيراً نقوم بإنشاء الميثود getProfilePhotoAsBitmap وهي ميثود تقوم بأخذ الرابط وتقوم بتحميل الصورة وإرجاعها ك Bitmap لعرضها في Notification,ولهذا قمنا بالإستعانة بمكتبة Glide المختصة بعرض الصور

private Bitmap getProfilePhotoAsBitmap(String url) {
        Bitmap bitmap = null;
        try {
            bitmap = Glide.with(this).load(url).asBitmap().into(168, 168).get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

حان وقت التجربة 😀

نقوم بتشغيل تطبيق الأندرويد ثم نجعل “Sobhe” يعمل Follow ل “Ahmad” (يمكنك ادخالهم بشكل يدوي من Firebase Console كما فعلت على سبيل التجربة!)

وها هو إشعارنا اللطيف يظهر 😀

سنحاول لاحقاً التطرق الى بعض الدروس الأخرى من Cloud Functions ان شاء الله

بعض المصادر التي قد تهمك

1 2 3 4 5

رابط المشروع على Github

ملاحظة:المشروع على Github لا يحتوي على ملفات google-services.json ولهذا يجب عليك توليد هذا الملف من Firebase Console ووضعه في مشروع الأندرويد
قد تم إرفاق ملف fcm-cloud-functions-export-Data Structure وهو ملف يحتوي على قاعدة البيانات البسيطة الذي أنشأناها,يمكنك استيرادها  من Realtime Database عبر خيار Import Backup (لا تنسَ عمل نسخ احتياطي لقاعدة البيانات لديك قبل تنفيذ عملية الاستيراد)

 

عن 3llomi

Just A GEEK :)

شاهد أيضاً

تحميل المزيد عند السحب للأسفل | Pagination In Firebase

سنقوم في هذا الدرس بشرح عملية Pagination في Firebase باستخدام مكتبة Infinite-Fire ماهو Pagination؟ لنفترض …

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *