This repository has no description
1package notella
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "os"
9 "strings"
10 "time"
11
12 firebase "firebase.google.com/go/v4"
13 "firebase.google.com/go/v4/messaging"
14 ll "github.com/gwennlbh/label-logger-go"
15 "golang.org/x/oauth2"
16 "golang.org/x/oauth2/google"
17 "google.golang.org/api/option"
18)
19
20var firebaseClient *firebase.App
21var firebaseCtx = context.Background()
22
23const MaxTokensPerRequest = 490
24
25func (msg Message) SendToFirebase(groupId string, subs []Subscription) error {
26 if firebaseClient == nil || !config.HasValidFirebaseServiceAccount() {
27 return nil
28 }
29
30 fcm, err := firebaseClient.Messaging(firebaseCtx)
31 if err != nil {
32 return fmt.Errorf("while initializing FCM client: %w", err)
33 }
34
35 message := msg.FirebaseMessage(groupId)
36 tokens := make([]string, len(subs))
37 for i, sub := range subs {
38 tokens[i] = sub.FirebaseToken()
39 }
40
41 for _, tokensChunk := range chunkBy(tokens, MaxTokensPerRequest) {
42 go func(tokens []string) {
43 if len(tokens) == 0 {
44 return
45 }
46 message.Tokens = tokens
47 if config.DryRunMode {
48 ll.Warn("dry run mode enabled, not sending FCM message to %d tokens", len(tokens))
49 return
50 }
51 resp, err := fcm.SendEachForMulticast(firebaseCtx, &message)
52 if err != nil {
53 ll.ErrorDisplay("while sending FCM message", err)
54 } else if resp.FailureCount > 0 {
55 fcmErrors := make([]string, 0, resp.FailureCount)
56 for i, result := range resp.Responses {
57 if !result.Success {
58 if result.Error.Error() == "Requested entity was not found." {
59 if sub, found := FindSubscriptionByNativeToken(tokens[i], subs); found {
60 ll.Log("Deleting", "yellow", "invalid native subscription %s", tokens[i])
61 sub.Destroy()
62 }
63 } else {
64 fcmErrors = append(fcmErrors, fmt.Sprintf("%s: %s", tokens[i], result.Error))
65 }
66 }
67 }
68 if len(fcmErrors) > 0 {
69 ll.ErrorDisplay(
70 "some FCM messages failed for %d tokens",
71 fmt.Errorf("- %s", strings.Join(fcmErrors, "\n- ")),
72 resp.FailureCount,
73 )
74 }
75 }
76 }(tokensChunk)
77 }
78
79 return nil
80}
81
82func (msg Message) FirebaseMessage(groupId string) messaging.MulticastMessage {
83 clickAction := ""
84 if len(msg.Actions) > 0 {
85 clickAction = msg.Actions[0].Label
86 }
87 return messaging.MulticastMessage{
88 Data: map[string]string{
89 "original": msg.JSONString(),
90 },
91 Android: &messaging.AndroidConfig{
92 RestrictedPackageName: config.AppPackageId,
93 Notification: &messaging.AndroidNotification{
94 VibrateTimingMillis: []int64{}, // TODO
95 EventTimestamp: nil, // TODO
96 ClickAction: clickAction,
97 },
98 },
99 Notification: &messaging.Notification{
100 Title: msg.Title,
101 Body: msg.Body,
102 ImageURL: msg.Image,
103 },
104 }
105}
106
107type firebaseServiceAccount struct {
108 Type string `json:"type"`
109 ProjectId string `json:"project_id"`
110 PrivateKeyId string `json:"private_key_id"`
111 PrivateKey string `json:"private_key"`
112 ClientEmail string `json:"client_email"`
113 ClientId string `json:"client_id"`
114 AuthUri string `json:"auth_uri"`
115 TokenUri string `json:"token_uri"`
116 AuthProviderX509CertUrl string `json:"auth_provider_x509_cert_url"`
117 ClientX509CertUrl string `json:"client_x509_cert_url"`
118 UniverseDomain string `json:"universe_domain"`
119}
120
121func setupFirebaseClient() (err error) {
122 httpClient := http.DefaultClient
123 if os.Getenv("DEBUG") == "1" {
124 httpClient = &http.Client{
125 Transport: debugTransport{t: http.DefaultTransport},
126 }
127 }
128
129 ctxWithClient := context.WithValue(firebaseCtx, oauth2.HTTPClient, httpClient)
130 creds, err := google.CredentialsFromJSON(ctxWithClient, []byte(config.FirebaseServiceAccount), "https://www.googleapis.com/auth/firebase.messaging")
131 if err != nil {
132 return fmt.Errorf("while setting credentials: %w", err)
133 }
134
135 client := &http.Client{
136 Transport: &oauth2.Transport{
137 Source: creds.TokenSource,
138 Base: httpClient.Transport,
139 },
140 Timeout: 10 * time.Second,
141 }
142
143 firebaseClient, err = firebase.NewApp(firebaseCtx, nil,
144 option.WithCredentialsJSON([]byte(config.FirebaseServiceAccount)),
145 option.WithHTTPClient(client),
146 )
147 return
148}
149
150func (config Configuration) HasValidFirebaseServiceAccount() bool {
151 var serviceAccount firebaseServiceAccount
152 err := json.Unmarshal([]byte(config.FirebaseServiceAccount), &serviceAccount)
153 if err != nil {
154 return false
155 }
156 if serviceAccount.Type != "service_account" {
157 return false
158 }
159
160 if err = setupFirebaseClient(); err != nil {
161 return false
162 }
163
164 return true
165}
166
167func (sub Subscription) FirebaseToken() string {
168 return strings.TrimPrefix(strings.TrimPrefix(sub.Webpush.Endpoint, "apns://"), "firebase://")
169}
170
171func (sub Subscription) IsNative() bool {
172 return !sub.IsWebpush()
173}