Flutter, PocketBase ve FCM V1 ile Otomatik Bildirim Sistemi

Bu rehber, PocketBase backend’i üzerinde çalışan, Google Firebase Cloud Messaging (FCM) V1 API kullanarak Flutter uygulamasına hem manuel hem de zamanlanmış (otomatik) bildirimler gönderen modern bir sistemin nasıl kurulacağını anlatır.

🌟 Özellikler

  • Sunucusuz (Serverless): Ekstra bir Node.js veya Python sunucusu gerektirmez. Tüm mantık PocketBase Hooks (JS) içinde çalışır.

  • FCM V1 API: Google’ın en yeni ve güvenli protokolü (OAuth 2.0) kullanılır. Legacy API kapandığı için bu yöntem zorunludur.

  • Otomasyon: Her gün belirli saatte otomatik “Günün Sözü” bildirimi atar.

  • Kontrol Paneli: Otomatik mesajlar veritabanından tek tuşla açılıp kapatılabilir.

  • Güvenlik: API uç noktaları gizli bir Secret Key ile korunur.


BÖLÜM 1: Google Cloud Kurulumu (Kritik Adım)

Google artık statik “Server Key” vermiyor. Dinamik token üretmek için OAuth kurulumu şart.

  1. Google Cloud Console‘a gidin ve Firebase projenizi seçin.

  2. APIs & Services > Credentials menüsünden + CREATE CREDENTIALS -> OAuth client ID seçin.

    • Type: Web application.

    • Redirect URI: https://developers.google.com/oauthplayground (Bu adres zorunludur).

  3. Oluşan Client ID ve Client Secret‘ı not edin.

  4. OAuth Consent Screen menüsüne gidin ve uygulamanızı “PUBLISH APP” butonuna basarak yayına alın. (Bunu yapmazsanız Token 7 gün sonra ölür).

  5. Google OAuth Playground‘a gidin:

    • Ayarlardan kendi Client ID/Secret’ınızı girin.

    • Scope olarak https://www.googleapis.com/auth/firebase.messaging seçin.

    • Exchange authorization code for tokens diyerek Refresh Token alın.


BÖLÜM 2: PocketBase Veritabanı Şeması

Admin panelinden şu koleksiyonları (tabloları) oluşturun:

1. duyurular (Log Kayıtları)

Gönderilen bildirimlerin yedeğini tutar.

  • baslik (Text)

  • icerik (Text)

2. sozler (İçerik Havuzu)

Otomatik botun rastgele seçeceği cümleler.

  • metin (Text)

  • (İçine 5-10 tane güzel söz ekleyin)

3. sistem_ayarlari (Kontrol Merkezi)

Sistemi kod değiştirmeden yönetmek için.

  • otomatik_bildirim_aktif (Bool) -> Sistemi aç/kapa.

  • son_gonderim_tarihi (Date) -> Günde 1 kez kontrolü için.

  • (Sadece 1 adet kayıt oluşturun ve ID’sini not edin).


BÖLÜM 3: Backend Kodları (Hooks)

Bu kodlar PocketBase sunucusunda (örneğin Coolify terminalinde) çalıştırılacaktır. Dosya yollarına (/app/pb_hooks/) dikkat edilmelidir.

3.1. Manuel Bildirim Servisi (broadcast.pb.js)

Bu servis, dışarıdan (Postman, Flutter Admin App) gelen istekleri karşılar ve anlık bildirim atar.

JavaScript

// Dosya Yolu: /app/pb_hooks/broadcast.pb.js

routerAdd("POST", "/api/duyuru-yap", (c) => {
    // --- KONFIGURASYON ---
    const CLIENT_ID = "SİZİN_CLIENT_ID";
    const CLIENT_SECRET = "SİZİN_CLIENT_SECRET";
    const REFRESH_TOKEN = "SİZİN_REFRESH_TOKEN";
    const PROJECT_ID = "FIREBASE_PROJE_ID"; // örn: justtalk-1234
    const ADMIN_API_KEY = "GIZLI_ADMIN_SIFRENIZ"; // örn: Just+Talk#2025

    try {
        // 1. GÜVENLİ VERİ OKUMA
        let title = "Başlık Yok";
        let body = "İçerik Yok";
        let secret = "";

        try {
            const rawBody = readerToString(c.request.body);
            if (rawBody) {
                const json = JSON.parse(rawBody);
                if (json.title) title = json.title;
                if (json.body) body = json.body;
                if (json.secret) secret = json.secret;
            }
        } catch (e) {}

        // 2. GÜVENLİK DUVARI (BODYGUARD)
        if (secret !== ADMIN_API_KEY) {
            return c.json(403, { error: "Yasaklı Giriş! Şifre Yanlış." });
        }

        // 3. GOOGLE TOKEN ALMA
        const tokenResponse = $http.send({
            url: "https://oauth2.googleapis.com/token",
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: `client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token`
        });

        if (tokenResponse.statusCode !== 200) return c.json(500, { error: "Token Hatası" });
        const accessToken = tokenResponse.json.access_token;

        // 4. BİLDİRİM GÖNDERME (Herkese)
        const fcmResponse = $http.send({
            url: `https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`,
            method: "POST",
            headers: {
                "Authorization": "Bearer " + accessToken,
                "Content-Type": "application/json"
            },
            body: JSON.stringify({
                "message": {
                    "topic": "tum_kullanicilar",
                    "notification": { "title": title, "body": body }
                }
            })
        });

        // 5. LOGLAMA
        if (fcmResponse.statusCode === 200) {
            try {
                const collection = $app.findCollectionByNameOrId("duyurular");
                const record = new Record(collection);
                record.set("baslik", title);
                record.set("icerik", body);
                $app.save(record);
            } catch (e) {}
        }

        return c.json(fcmResponse.statusCode, fcmResponse.json);
    } catch (err) {
        return c.json(500, { error: err.toString() });
    }
});

3.2. Otomatik Zamanlayıcı (scheduler.pb.js)

Bu servis her dakika uyanır, veritabanını kontrol eder ve şartlar uygunsa rastgele bir söz seçip gönderir.

JavaScript

// Dosya Yolu: /app/pb_hooks/scheduler.pb.js

// Her dakika çalış (* * * * *)
cronAdd("otomatik_olumlama", "* * * * *", (c) => {
    // --- KONFIGURASYON ---
    const CLIENT_ID = "SİZİN_CLIENT_ID";
    const CLIENT_SECRET = "SİZİN_CLIENT_SECRET";
    const REFRESH_TOKEN = "SİZİN_REFRESH_TOKEN";
    const PROJECT_ID = "FIREBASE_PROJE_ID";
    const AYAR_ID = "SISTEM_AYARLARI_KAYIT_ID"; // PocketBase'deki ID

    try {
        // 1. KONTROL: Bugün gönderildi mi? Sistem açık mı?
        let ayarKaydi;
        try { ayarKaydi = $app.findRecordById("sistem_ayarlari", AYAR_ID); } catch(e) { return; }

        if (ayarKaydi.getBool("otomatik_bildirim_aktif") !== true) return;

        const sonTarihStr = ayarKaydi.getString("son_gonderim_tarihi");
        if (sonTarihStr) {
            const sonTarih = new Date(sonTarihStr);
            const bugun = new Date();
            if (sonTarih.getDate() === bugun.getDate() && 
                sonTarih.getMonth() === bugun.getMonth() && 
                sonTarih.getFullYear() === bugun.getFullYear()) {
                return; // Bugün zaten gönderilmiş.
            }
        }

        // 2. İÇERİK SEÇİMİ (Rastgele)
        let secilenSoz = "Harika bir gün!";
        const records = $app.findAllRecords("sozler");
        if (records.length > 0) {
            const randomIndex = Math.floor(Math.random() * records.length);
            secilenSoz = records[randomIndex].getString("metin");
        }

        // 3. TOKEN ALMA
        const tokenResponse = $http.send({
            url: "https://oauth2.googleapis.com/token",
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: `client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token`
        });
        if (tokenResponse.statusCode !== 200) return;
        const accessToken = tokenResponse.json.access_token;

        // 4. GÖNDERME (Kanal: gunluk_olumlama)
        const fcmResponse = $http.send({
            url: `https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`,
            method: "POST",
            headers: {
                "Authorization": "Bearer " + accessToken,
                "Content-Type": "application/json"
            },
            body: JSON.stringify({
                "message": {
                    "topic": "gunluk_olumlama",
                    "notification": { "title": "Günün Sözü ✨", "body": secilenSoz }
                }
            })
        });

        // 5. MÜHÜRLEME (Tarihi Güncelle)
        if (fcmResponse.statusCode === 200) {
            ayarKaydi.set("son_gonderim_tarihi", new Date().toISOString());
            $app.save(ayarKaydi);
            console.log("✅ Günlük bildirim başarıyla gönderildi.");
        }

    } catch (err) {
        console.log("🔥 Cron Hatası: " + err.toString());
    }
});

BÖLÜM 4: Flutter Entegrasyonu

4.1. Ayarlar Sayfası (Topic Aboneliği)

Kullanıcının günlük bildirimleri açıp kapatabilmesi için.

Dart

// Switch değiştiğinde çalışacak fonksiyon
Future<void> _bildirimAyariniDegistir(bool acikMi) async {
  final fcm = FirebaseMessaging.instance;
  
  if (acikMi) {
    await fcm.subscribeToTopic('gunluk_olumlama');
    print("Abone olundu ✅");
  } else {
    await fcm.unsubscribeFromTopic('gunluk_olumlama');
    print("Abonelikten çıkıldı ❌");
  }
}

4.2. Admin Paneli (Manuel Gönderim)

Backend’deki şifre korumalı servise istek atma.

Dart

Future<void> _bildirimGonder() async {
  final url = Uri.parse('https://SİZİN-DOMAIN.com/api/duyuru-yap');
  final response = await http.post(
    url,
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'title': 'Test Başlık',
      'body': 'Test İçerik',
      'secret': 'Just+Talk#2025' // Backend'deki şifre ile aynı olmalı
    }),
  );
  
  if (response.statusCode == 200) {
    print("Gönderildi! 🚀");
  } else {
    print("Hata: ${response.body}");
  }
}

💡 İpuçları ve Sorun Giderme

  1. Klasör Yolu: Coolify veya Docker ortamında dosyalar her zaman /app/pb_hooks/ klasöründe olmalıdır. Terminalde mkdir -p /app/pb_hooks komutunu kullanarak klasörü garantiye alın.

  2. Restart: .js dosyalarında yapılan her değişiklikten sonra PocketBase sunucusu Restart edilmelidir.

  3. Cron Formatı: PocketBase Goja motoru 5 yıldızlı formatı (* * * * *) destekler. @every gibi makrolar bazı sürümlerde çalışmayabilir.

  4. Veritabanı Güvenliği: JS Hook’ları $app objesini kullandığı için Admin yetkisine sahiptir, API Rules kısıtlamalarına takılmaz.

Yorum bırakın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir