feat: complete IM P0 and P1 reliability work

This commit is contained in:
richjohnrambo
2026-08-17 13:43:27 +08:00
parent ddf0f7a55e
commit f2c229135b
43 changed files with 4512 additions and 1181 deletions
+7
View File
@@ -17,6 +17,13 @@ This adapter requires you to obtain your own credentials from Goole Firebase. If
Remove the other entry. I.e. if you have updated `"credentials_file"`, remove `"credentials"` and vice versa.
4. Update [TinodeWeb](/tinode/webapp/) config [`firebase-init.js`](https://github.com/tinode/webapp/blob/master/firebase-init.js): update `apiKey`, `messagingSenderId`, `projectId`, `appId`, `messagingVapidKey`. See more info at https://github.com/tinode/webapp/#push_notifications
The server may also enable FCM without storing credentials in `tinode.conf`:
* `IM_FCM_CREDENTIALS_FILE` points to the service-account JSON file.
* `IM_FCM_CREDENTIALS_JSON` contains the service-account JSON directly.
Either variable enables the adapter even when the checked-in configuration has `enabled: false`.
### iOS and Android
1. If you are using an Android client, add `google-services.json` to [Tindroid](/tinode/tindroid/) by following instructions at https://developers.google.com/android/guides/google-services-plugin and recompile the client. You may also optionally submit it to Google Play Store.
+15 -2
View File
@@ -123,7 +123,7 @@ func PrepareV1Notifications(rcpt *push.Receipt, config *configType) ([]*fcmv1.Me
skipDevices[deviceID] = struct{}{}
}
}
devices, count, err = store.Devices.GetAll(rcpt.TenantID, uids...)
devices, count, err = pushDevicesForUsers(rcpt.TenantID, uids...)
if err != nil {
logs.Warn.Println("fcm push: db error", err)
return nil, nil
@@ -218,7 +218,7 @@ func TenantChannelName(tenantID t.TenantID, channel string) string {
// DevicesForUser loads device IDs of the given user.
func DevicesForUser(tenantID t.TenantID, uid t.Uid) []string {
ddef, count, err := store.Devices.GetAll(tenantID, uid)
ddef, count, err := pushDevicesForUsers(tenantID, uid)
if err != nil {
logs.Warn.Println("fcm devices for user: db error", err)
return nil
@@ -235,6 +235,19 @@ func DevicesForUser(tenantID t.TenantID, uid t.Uid) []string {
return devices
}
func pushDevicesForUsers(tenantID t.TenantID, uids ...t.Uid) (map[t.Uid][]t.DeviceDef, int, error) {
if tokens, ok := store.DevicePushTokens.(store.DevicePushTokenQueryInterface); ok {
devices, count, err := tokens.GetAll(tenantID, uids...)
if err != nil {
return nil, 0, err
}
if count > 0 {
return devices, count, nil
}
}
return store.Devices.GetAll(tenantID, uids...)
}
// ChannelsForUser loads user's channel subscriptions with P permission.
func ChannelsForUser(tenantID t.TenantID, uid t.Uid) []string {
channels, err := store.Users.GetChannels(tenantID, uid)
+65
View File
@@ -0,0 +1,65 @@
package fcm
import (
"testing"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
type testProductPushTokens struct {
devices map[types.Uid][]types.DeviceDef
}
func (s *testProductPushTokens) Upsert(types.TenantID, *types.DevicePushToken) (*types.DevicePushToken, error) {
return nil, nil
}
func (s *testProductPushTokens) GetAll(_ types.TenantID, _ ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
count := 0
for _, devices := range s.devices {
count += len(devices)
}
return s.devices, count, nil
}
type testLegacyPushDevices struct {
called bool
}
func (*testLegacyPushDevices) Update(types.TenantID, types.Uid, string, *types.DeviceDef) error {
return nil
}
func (s *testLegacyPushDevices) GetAll(types.TenantID, ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
s.called = true
return nil, 0, nil
}
func (*testLegacyPushDevices) Delete(types.TenantID, types.Uid, string) error { return nil }
func TestPushDevicesForUsersPrefersProductTokens(t *testing.T) {
oldTokens := store.DevicePushTokens
oldDevices := store.Devices
defer func() {
store.DevicePushTokens = oldTokens
store.Devices = oldDevices
}()
uid := types.Uid(42)
legacy := &testLegacyPushDevices{}
store.DevicePushTokens = &testProductPushTokens{devices: map[types.Uid][]types.DeviceDef{
uid: {{TenantID: 7, DeviceId: "product-token", Platform: "android"}},
}}
store.Devices = legacy
devices, count, err := pushDevicesForUsers(7, uid)
if err != nil {
t.Fatal(err)
}
if count != 1 || devices[uid][0].DeviceId != "product-token" {
t.Fatalf("unexpected devices: %#v count=%d", devices, count)
}
if legacy.called {
t.Fatal("legacy device store must not be read when product tokens exist")
}
}
+8
View File
@@ -70,6 +70,14 @@ func (Handler) Init(jsonconf json.RawMessage) (bool, error) {
return false, errors.New("failed to parse config: " + err.Error())
}
if raw := strings.TrimSpace(os.Getenv("IM_FCM_CREDENTIALS_JSON")); raw != "" {
config.Credentials = json.RawMessage(raw)
config.Enabled = true
}
if path := strings.TrimSpace(os.Getenv("IM_FCM_CREDENTIALS_FILE")); path != "" {
config.CredentialsFile = path
config.Enabled = true
}
if !config.Enabled {
return false, nil
}