方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

[Go x FCM] Implemented a function to send push notifications to FCM on a regular basis: Server Edition

flutter-fcm-push-notifications
この記事は約27分で読めます。

Introduction.

Previously, in this article, we described the process of using Firebase Cloud Messaging (FCM) to send test transmissions in the background and foreground to the actual iOS device.

This time, we would like to implement a function to send push notifications from the server to FCM at regular times in the Go language.

The following four arguments are required to implement Push notifications in FCM

  • FCM Server Key: Authorization of HTTP request header
  • FCM token for the device: as a designation of the destination of the message
  • Title.
  • body (of letter)

Obtain FCM service account credentials

For API requests to FCM, the FCM server key was previously set as the Authorization in the HTTP request header, but this Cloud Messaging API (Legacy HTTP API) will be discontinued on June 20, 2024, and the FCM server However, this Cloud Messaging API (Legacy HTTP API) will be discontinued on June 20, 2024, and the FCM server key will no longer be available, so the HTTP v1 API must be used.

The HTTP v1 API is done via Oauth2.0 tokens, and FCM credentials (usually in JSON format) must be obtained from the Google Cloud console for OAuth2.0 token generation.

Click Google Cloud Console → IAM and Administration → Create Service Account

Fill out the required information and click Done.

  • Service Account Name: Enter a descriptive name (e.g., FCM Service Account)
  • Service Account ID: The same name is automatically populated when a service account name is entered, but can be customized as needed.
  • Required Roles: Roles related to Firebase Cloud Messaging (e.g., “Firebase Cloud Messaging Admin”)

Select the created service account, click the “Key” tab, click “Add Key”, click “Create New Key”, select JSON format, and the download will start automatically.

Store the downloaded JSON file in an appropriate directory, such as in .credential, so that it can be called up.

Send FCM token to the server

When sending push notifications from the server to the FCM backend, the FCM token identifying the individual device must be passed as an argument.

Add a new column called FCM token to the User table stored on the server, so that it is stored in the DB when the user starts the application for the first time, and implement the process of calling from the FCM token column in the DB when actually sending notifications.

According to the best practices for FCM registration token management, it is better to store the timestamp on the server along with the FCM token, so store the timestamp as well.

The app must obtain this token on first launch and store the token along with a timestamp on the app server. This timestamp is not provided by the FCM SDK and must be implemented by your own code and server.

https://firebase.google.com/docs/cloud-messaging/manage-tokens?hl=ja

Go: Add columns to store FCM tokens and timestamps in DB

ShellScript
$ cd scripts/migrations
$ migrate create -ext sql -format 2006010215 add_fcm_token_to_users

The up-and-down sql files are created and described as follows.

Go
// 2024050323_add_fcm_token_to_users.up.sql
ALTER TABLE users
    ADD COLUMN fcm_token VARCHAR(255),
    ADD COLUMN fcm_token_timestamp TIMESTAMP;
Go
// 2024050323_add_fcm_token_to_users.down.sql
ALTER TABLE users
    DROP COLUMN fcm_token,
    DROP COLUMN fcm_token_timestamp;

I applied migration and added fcm_token and fcm_token_timestamp columns to the user table.

Go: Add fcm_token field to User structure

Add fcm_token field to Go’s User structure so that the fcm_token field can be read and written from the database.

Go
// user.go
type User struct {
    UID                        string `db:"uid"`
    DeviceID                   string `db:"device_id"`
    Name                       string `db:"name"`
    // ... 他のフィールド
    FcmToken                   types.NullString `db:"fcm_token"`
    FcmTokenTimestamp          *time.Time      `db:"fcm_token_timestamp" json:"fcm_token_timestamp"`
}

Go: Update Update UpdateUser function to handle fcm_token.

Go
// user_db.go
func UpdateUser(db *sqlx.DB, user *User) (*User, error) {
	tx, err := db.Beginx()
	if err != nil {
		return nil, err
	}

	query := `update users
			  set device_id				    = coalesce(:device_id, device_id),
			  ...
			  fcm_token = coalesce(:fcm_token, fcm_token),
			  fcm_token_timestamp		= coalesce(:fcm_token_timestamp, fcm_token_timestamp),
			  ...
			  WHERE uid = :uid;`
	return &updatedUser, nil
}

Flutter: Send FCM token to the server when the app starts

Treat FCM tokens as part of the user data and manage them in API requests using the ApiClient class.

Ensure that Dart’s User the model includes a field for FCM tokens and updates this model to allow serialization and deserialization of this field.

Go
// lib/api/user.dart
class User {
  final String? uid;
  final String? fcmToken;
  final DateTime? fcmTokenTimestamp;

	User(
      {this.uid,
			 this.fcmToken,
       this.fcmTokenTimestamp});
}

Use build_runner to generate JSON serialization code (user.g.dart).

ShellScript
flutter pub run build_runner build --delete-conflicting-outputs

Next, implement in main.dart The functionality is to retrieve the FCM token and send it to the server when the app is launched. updateUser method of UserNotifier already has the functionality to receive and update the User object, so we will use this method to update the user’s Update FCM token.

Dart
import 'package:clocky_app/api/user.dart';
import 'package:clocky_app/api/user_notifier.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:overlay_support/overlay_support.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(ProviderScope(child: const ClockyApp()));
}

class ClockyApp extends ConsumerStatefulWidget {
  const ClockyApp({super.key});

  @override
  _ClockyAppState createState() => _ClockyAppState();
}

class _ClockyAppState extends ConsumerState<ClockyApp> {
  @override
  void initState() {
    super.initState();
    initializeApp();
  }

  Future<void> initializeApp() async {
    final messaging = FirebaseMessaging.instance;
    NotificationSettings settings = await messaging.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );
    debugPrint('User granted permission: ${settings.authorizationStatus}');

    final token = await messaging.getToken();
    debugPrint('FCM TOKEN: $token');

    if (token != null) {
    // FCMトークンをサーバーに送る
      final userNotifier = ref.read(userProvider.notifier);
      userNotifier.updateUser(User(fcmToken: token, fcmTokenTimestamp: DateTime.now()));
    }
  }
}

flutter run and verify that fcm_token and fcm_token_timestamp are correctly stored and updated in the DB.

In the flutter startup log, an updateUser request is sent to the server.

ShellScript
flutter: Request updateUser: {"fcm_token":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}

We confirmed that fcm_token and fcm_token_timestamp were successfully stored in the DB.

Go: Implement message notification function to FCM (HTTP v1 API supported)

Proceed with implementation (fcm.go) to send notifications from the server to FCM in the Go language.

This time, since the application is running on AWS instead of GCP, it corresponds to the ” case where the application is running on a server environment other than Google’s ” in the following explanation article.

https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ja&_gl=11od2z5e_upMQ.. _gaMTQyOTM0NjcyLjE3MTQ4ODYxMDI._ga_CW55HF8NVT*MTcxNDg4NjEwMi4xLjAuMTcxNDg4NjEwMi4wLjAuMA

Server endpoints for HTTP v1 are as follows

JSON
POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send

Use the OAuth 2.0 token obtained through the FCM service authentication information obtained earlier in the HTTP request header.

Use the google.golang.org/api/fcm/v1 package and use the WithCredentialsJSON method to read the JSON file of FCM service account credentials.

https://pkg.go.dev/google.golang.org/api@v0.177.0/option#WithCredentialsJSON

The SendFCMNotification function is used to send a notification to the FCM; it takes the FCM token, title, and message body as parameters and uses them to send a push notification.

The HTTP v1 API also changes the structure of the JSON message payload. To target a specific device, specify the device’s current registration token in the message object with the token key.

Dart
  { "notification": {
      "body": "This is an FCM notification message!",
      "time": "FCM Message"
    },
    "to" : "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1..."
  }

The entire code is here.

Go
package clocky_be

import (
	"context"
	"fmt"
	"log"

	fcm "google.golang.org/api/fcm/v1"
	"google.golang.org/api/option"
)


func SendFCMNotification(ctx context.Context, config *Config,token, title, body string) error {
	opt := option.WithCredentialsJSON([]byte(config.GoogleApplicationCredentials))
	service, err := fcm.NewService(ctx, opt)
	if err != nil {
		return fmt.Errorf("could not create FCM service: %v", err)
	}

	// Construct the message to be sent.
	message := &fcm.Message{
		Token: token,
		Notification: &fcm.Notification{
			Title: title,
			Body:  body,
		},
	}

	// Create a new "messages" client from the FCM service.
	messagesClient := fcm.NewProjectsMessagesService(service)

	request := &fcm.SendMessageRequest{
		Message: message,
	}
	fmt.Printf("FCM log")

	// Send the message using the FCM v1 API endpoint
	response, err := messagesClient.Send(fmt.Sprintf("projects/%s", config.FirebaseProjectId), request).Do()
	if err != nil {
		log.Printf("Request failed: %+v", request)
		return fmt.Errorf("failed to send notification: %v", err)
	}

	fmt.Printf("Successfully sent notification. Response: %+v\n", response)
	return nil
}

Function implementation to send weather information to FCM

Implement a function to send the weather information implemented this time to FCM using the SendFCMNotification function.

Add the FCM server key to the .env file.

Go
FCM_SERVER_KEY=your_fcm_server_key_here

This time, the weather information message to be pushed to the user is already stored in the weather_message column of the DB’s user table, so it is called from the WeatherMessage field of the User structure. The message title is fixed to “Today’s weather information.

Go
// worker.go
// FCMを使用したリアルタイム天気通知の送信
func SendFCMWeatherNotification(ctx context.Context, db *sqlx.DB, message Message, config *Config) {
    user, err := GetUser(db, message.UserID)
    if err != nil {
        log.Printf("Failed to get user info for FCM notification: %v", err)
        return
    }

    // ユーザーのFCMトークンの存在を確認
    if !user.FcmToken.Valid {
        log.Printf("No FCM token available for user %s", user.UID)
        return
    }

    // 天気情報の更新が必要かどうかを確認
    if user.WeatherMessage.Valid && user.WeatherMessage.String != "" {
        // 天気情報をFCMで送信
        if err := SendFCMNotification(ctx, config, user.FcmToken.String, "[ミーア]今日の天気お知らせ", user.WeatherMessage.String); err != nil {
            log.Printf("Failed to send FCM notification for user %s: %v", user.UID, err)
            return
        }
        log.Printf("FCM notification sent for user: %s", user.UID)
    } else {
        log.Printf("No valid weather message to send for user %s", user.UID)
    }
}

Added periodic execution processing

Ensure that weather information is sent to the user via FCM at a specific time ( based on the WeatherAnnouncementTime stored in the database).

ScheduleTask function: Scheduling a task

  • clocky.ScheduleTask(db, messagesChan) retrieves from the database only those users whose weather_announcement_time exactly matches the current time and adds the FCM notification task to the queue.
Go
// worker.go
func ScheduleTask(db *sqlx.DB, messagesChan chan Message) {
		var users []User
		query := `SELECT * FROM users WHERE DATE_FORMAT(weather_announcement_time, '%H:%i') = DATE_FORMAT(?, '%H:%i');`
		err = db.Select(&users, query, currentTimeJST.String())
    for _, user := range users {
			if user.WeatherAnnouncementTime.Valid {
				log.Printf("Sending FCM weather notification for user: %s", user.UID)
				message := Message{
					UserID: user.UID,
					Type:   "weather_fcm",
				}
				messagesChan <- message
			}
    }
    // ...
}

ProcessMessages function: Process messages

  • ProcessMessages(db, messagesChan, config) performs appropriate actions based on messages received from the message channel. This function processes different types of messages (FCM notifications of weather information and other processes)
Go
// worker.go
// メッセージ処理の抽象化
func ProcessMessages(db *sqlx.DB, messagesChan chan Message, config *Config) {
    for message := range messagesChan {
        log.Printf("Processing message for user: %s, type: %s", message.UserID, message.Type)

        // タイムアウトを設定
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
        defer cancel()

        switch message.Type {
        case "weather_fcm":
            SendFCMWeatherNotification(ctx, db, message, config)
        default:
            log.Printf("Unknown message type: %s", message.Type)
        }
    }
}

Finally, set up the Cron job in main.go.

Go: Setup and start a Cron job

Initialize and schedule a Cron job: Create a Cron instance that supports second-by-second scheduling with cron.New and set it to run clocky.ScheduleTask at 0 seconds of every minute. This function schedules a recurring task (for example, updating a user’s weather information).

Go
// main.go
package main

import (
	"context"
	"log"
	"github.com/jmoiron/sqlx"
	"github.com/robfig/cron/v3"
	"github.com/EarEEG-dev/clocky_be"
)

func main() {
	ctx := context.Background()
	config := clocky.NewConfig()

	db, err := sqlx.Open("mysql", config.DSN)
	if err != nil {
		log.Fatalf("unable to connect to database: %v", err)
	}

	if err := db.Ping(); err != nil {
		log.Fatalf("unable to ping database: %v", err)
	}
	log.Printf("connected to database")

	// メッセージチャネルとCronジョブの設定
	messagesChan := make(chan clocky.Message)

	// 秒単位をサポートしてcronを初期化
	c := cron.New(cron.WithSeconds())

	// 毎分0秒時点で起動
	c.AddFunc("0 * * * * *", func() {
		log.Println("Cron job triggered")
		clocky.ScheduleTask(db, messagesChan)
	})
	go c.Start()

	// 処理関数の起動数を増やして並列に処理させることも可能
	go clocky.ProcessMessages(db, messagesChan, config)

	// その他のアプリケーション初期化コード(HTTPサーバー、SQSリスナー、gRPCサーバー、Rest API)
	// ...
}

operation check

Verify whether the app receives a Push notification at the specified time by changing the weather notification time to 13:53 in the app.
The notification was received in the foreground.
Push notifications sent from the server to FCM are sent to the user’s device in real-time.

Next, change the notification time to 13:54 and put the app in the background state.
It arrived safely.

Finally, put it in a locked screen state. It arrived safely.

コメント

タイトルとURLをコピーしました