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

AbdulAlim Rajjoubمنذ 7 سنوات

ماهي 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

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


FCM-Cloud-Functions-1.thumb.png.5048377a17a472473c09e5feda99c134.png

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


firebase login

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

FCM-Cloud-Functions-2.thumb.png.e3c745247834a51fe8b069930994ea43.png
 

اختر Allow


FCM-Cloud-Functions-3.png.3a728c627a6f306eaf357437827ee269.png

 

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


FCM-Cloud-Functions-4.png.ee9012f727c0cfab2d3fc045a40d219e.png
 

FCM-Cloud-Functions-5.thumb.png.74721b13f9e002b9d4da42422eb6f4e2.png

 

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

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


firebase init functions


FCM-Cloud-Functions-6.thumb.png.bec3495975884f573b8eeb5b369e9327.png

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

FCM-Cloud-Functions-7.thumb.png.35d30598db3f0768ad626809ae414d53.png

ثم اختر Y

FCM-Cloud-Functions-8.thumb.png.c65ad58b9952ccb04264d53dcfee6e8a.png

 

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

FCM-Cloud-Functions-9.thumb.png.0a33dce2b8ee0304f02dc513f5d4b394.png

 

الآن اذا ذهبنا الى المجلد سنجد به بعض الملفات
FCM-Cloud-Functions-10.png.5546534eaaf9cda4be93e2bb8cfcec9b.png
 

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

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

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

FCM-Cloud-Functions-11.thumb.png.64fae15c18aa189ab5d7f613a2c68579.png
 

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

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

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

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

  • notificationTokens
  • photo رابط صورة المستخدم
  • userName اسم المستخدم

FCM-Cloud-Functions-Data-Structure.png.641f37e439c83a366c048ec7ff8f6703.png

 

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

 

FCM-Followers.png.4d8f83ffe9aaf93c55d1679f4d519558.png

 

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

ولكن قبل هذا سأذكرك بأن 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)

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

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

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

example-follows.png.5589e08cbbd774807e96e57a094f20a2.png

 

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


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

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

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

fcm-90-1.png.87a2e4578f36b6a6c4f007a2821fe927.png

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


 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 او IOS او Web (سنشرح بشكل بسيط عن طريق الأندرويد)

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

  • 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 وقد تأخذ العملية بعض الوقت

FCM-Deploy.thumb.png.e4ddd4b7a05e32b5e0f77d64c6681aec.png

 

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

 

FCM-Console.thumb.png.7e73c3ed5df1a70e2eec08f2d8442ecb.png

 

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

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

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

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

FCM-Android-1.thumb.png.77fd0256b3eebe994d6c476ea9221a6b.png

 

ثم استدعينا ميثود سنقوم بإنشاءها وهي 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 كما فعلت على سبيل التجربة!)

FCM-Android-Running.png.5ef651698583df93376ae9ef5d71ecf1.png

 

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

ملاحظة:

قد تم إرفاق ملف fcm-cloud-functions-export-Data Structure وهو ملف يحتوي على قاعدة البيانات البسيطة الذي أنشأناها,يمكنك استيرادها  من Realtime Database عبر خيار Import Backup (لا تنسَ عمل نسخ احتياطي لقاعدة البيانات لديك قبل تنفيذ عملية الاستيراد)

 

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

1,2,3

كلمات دليلية:
4
إعجاب
7723
مشاهدات
0
مشاركة
3
متابع
متميز
محتوى رهيب

التعليقات (5)

Barakah:

شرح مفصل ..شكرا جزيلا 

حابه اتواصل معك عندي أسئلة كثيره ..هل بالإمكان ع تويتر او تيلقرام؟

AbdulAlim Rajjoub:

أكيد ,وسائل التواصل موجودة في حسابي على عالم البرمجة 

عبدالرحمن الخالدي:

السلام عليكم اخوي احصل عندك رابط مشروع لكن للـ IOS ؟

عمرو الجنيات:

هل استطيع إرسال notification من خلال ال (uid) الخاصة بالمستخدم؟

لأني مش عارف اجيب ال token لأن اتطبيق لدي لا يقوم المستخدم ب إنشاء حساب , بل يتم إنشاء الحسابات عن طريق الادمن فقط
ارجو المساعدة, وشكرا

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !