This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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 if config.DryRunMode && len(config.DryRunExceptions) == 0 { 36 ll.Warn("dry run mode enabled, not sending FCM message to %d tokens", len(subs)) 37 return nil 38 } 39 40 message := msg.FirebaseMessage(groupId) 41 tokens := make([]string, 0, len(subs)) 42 for _, sub := range subs { 43 if config.DryRunMode { 44 exempt := false 45 for _, username := range config.DryRunExceptions { 46 if username == sub.Owner.Uid { 47 exempt = true 48 } 49 } 50 if !exempt { 51 continue 52 } 53 } 54 tokens = append(tokens, sub.FirebaseToken()) 55 } 56 57 if config.DryRunMode { 58 ll.Warn("dry run mode enabled, only sending FCM message to %d tokens (owned by %+v)", len(tokens), config.DryRunExceptions) 59 } 60 61 for _, tokensChunk := range chunkBy(tokens, MaxTokensPerRequest) { 62 go func(tokens []string) { 63 if len(tokens) == 0 { 64 return 65 } 66 message.Tokens = tokens 67 resp, err := fcm.SendEachForMulticast(firebaseCtx, &message) 68 if err != nil { 69 ll.ErrorDisplay("while sending FCM message", err) 70 } else if resp.FailureCount > 0 { 71 fcmErrors := make([]string, 0, resp.FailureCount) 72 for i, result := range resp.Responses { 73 if !result.Success { 74 if result.Error.Error() == "Requested entity was not found." { 75 if sub, found := FindSubscriptionByNativeToken(tokens[i], subs); found { 76 ll.Log("Deleting", "yellow", "invalid native subscription %s", tokens[i]) 77 sub.Destroy() 78 } 79 } else { 80 fcmErrors = append(fcmErrors, fmt.Sprintf("%s: %s", tokens[i], result.Error)) 81 } 82 } 83 } 84 if len(fcmErrors) > 0 { 85 ll.ErrorDisplay( 86 "some FCM messages failed for %d tokens", 87 fmt.Errorf("- %s", strings.Join(fcmErrors, "\n- ")), 88 resp.FailureCount, 89 ) 90 } 91 } 92 }(tokensChunk) 93 } 94 95 return nil 96} 97 98func (msg Message) FirebaseMessage(groupId string) messaging.MulticastMessage { 99 clickAction := "" 100 if len(msg.Actions) > 0 { 101 clickAction = msg.Actions[0].Label 102 } 103 return messaging.MulticastMessage{ 104 Data: map[string]string{ 105 "original": msg.JSONString(), 106 }, 107 Android: &messaging.AndroidConfig{ 108 RestrictedPackageName: config.AppPackageId, 109 Notification: &messaging.AndroidNotification{ 110 VibrateTimingMillis: []int64{}, // TODO 111 EventTimestamp: nil, // TODO 112 ClickAction: clickAction, 113 }, 114 }, 115 Notification: &messaging.Notification{ 116 Title: msg.Title, 117 Body: msg.Body, 118 ImageURL: msg.Image, 119 }, 120 } 121} 122 123type firebaseServiceAccount struct { 124 Type string `json:"type"` 125 ProjectId string `json:"project_id"` 126 PrivateKeyId string `json:"private_key_id"` 127 PrivateKey string `json:"private_key"` 128 ClientEmail string `json:"client_email"` 129 ClientId string `json:"client_id"` 130 AuthUri string `json:"auth_uri"` 131 TokenUri string `json:"token_uri"` 132 AuthProviderX509CertUrl string `json:"auth_provider_x509_cert_url"` 133 ClientX509CertUrl string `json:"client_x509_cert_url"` 134 UniverseDomain string `json:"universe_domain"` 135} 136 137func setupFirebaseClient() (err error) { 138 httpClient := http.DefaultClient 139 if os.Getenv("DEBUG") == "1" { 140 httpClient = &http.Client{ 141 Transport: debugTransport{t: http.DefaultTransport}, 142 } 143 } 144 145 ctxWithClient := context.WithValue(firebaseCtx, oauth2.HTTPClient, httpClient) 146 creds, err := google.CredentialsFromJSON(ctxWithClient, []byte(config.FirebaseServiceAccount), "https://www.googleapis.com/auth/firebase.messaging") 147 if err != nil { 148 return fmt.Errorf("while setting credentials: %w", err) 149 } 150 151 client := &http.Client{ 152 Transport: &oauth2.Transport{ 153 Source: creds.TokenSource, 154 Base: httpClient.Transport, 155 }, 156 Timeout: 10 * time.Second, 157 } 158 159 firebaseClient, err = firebase.NewApp(firebaseCtx, nil, 160 option.WithCredentialsJSON([]byte(config.FirebaseServiceAccount)), 161 option.WithHTTPClient(client), 162 ) 163 return 164} 165 166func (config Configuration) HasValidFirebaseServiceAccount() bool { 167 var serviceAccount firebaseServiceAccount 168 err := json.Unmarshal([]byte(config.FirebaseServiceAccount), &serviceAccount) 169 if err != nil { 170 return false 171 } 172 if serviceAccount.Type != "service_account" { 173 return false 174 } 175 176 if err = setupFirebaseClient(); err != nil { 177 return false 178 } 179 180 return true 181} 182 183func (sub Subscription) FirebaseToken() string { 184 return strings.TrimPrefix(strings.TrimPrefix(sub.Webpush.Endpoint, "apns://"), "firebase://") 185} 186 187func (sub Subscription) IsNative() bool { 188 return !sub.IsWebpush() 189}