多租户更改

This commit is contained in:
richjohnrambo
2026-08-11 15:59:57 +08:00
parent 6c800de67d
commit c2076721fc
453 changed files with 102349 additions and 47 deletions
+543
View File
@@ -0,0 +1,543 @@
package common
import (
"fmt"
"net/http"
"reflect"
"strings"
"github.com/tinode/chat/server/push"
"google.golang.org/api/googleapi"
)
// Payload to be sent for a specific notification type.
type Payload struct {
// Common for APNS and Android
Body string `json:"body,omitempty"`
Title string `json:"title,omitempty"`
TitleLocKey string `json:"title_loc_key,omitempty"`
TitleLocArgs []string `json:"title_loc_args,omitempty"`
// Android
BodyLocKey string `json:"body_loc_key,omitempty"`
BodyLocArgs []string `json:"body_loc_args,omitempty"`
Icon string `json:"icon,omitempty"`
Color string `json:"color,omitempty"`
ClickAction string `json:"click_action,omitempty"`
Sound string `json:"sound,omitempty"`
Image string `json:"image,omitempty"`
// APNS
Action string `json:"action,omitempty"`
ActionLocKey string `json:"action_loc_key,omitempty"`
LaunchImage string `json:"launch_image,omitempty"`
LocArgs []string `json:"loc_args,omitempty"`
LocKey string `json:"loc_key,omitempty"`
Subtitle string `json:"subtitle,omitempty"`
SummaryArg string `json:"summary_arg,omitempty"`
SummaryArgCount int `json:"summary_arg_count,omitempty"`
}
// Config is the configuration of a Notification payload.
type Config struct {
Enabled bool `json:"enabled,omitempty"`
// Common defaults for all push types.
Payload
// Configs for specific push types.
Msg Payload `json:"msg,omitempty"`
Sub Payload `json:"sub,omitempty"`
}
func (cp Payload) getStringAttr(field string) string {
val := reflect.ValueOf(cp).FieldByName(field)
if !val.IsValid() {
return ""
}
if val.Kind() == reflect.String {
return val.String()
}
return ""
}
func (cp Payload) getIntAttr(field string) int {
val := reflect.ValueOf(cp).FieldByName(field)
if !val.IsValid() {
return 0
}
if val.Kind() == reflect.Int {
return int(val.Int())
}
return 0
}
func (cc *Config) GetStringField(what, field string) string {
var val string
if what == push.ActMsg {
val = cc.Msg.getStringAttr(field)
} else if what == push.ActSub {
val = cc.Sub.getStringAttr(field)
}
if val == "" {
val = cc.Payload.getStringAttr(field)
}
return val
}
func (cc *Config) GetIntField(what, field string) int {
var val int
if what == push.ActMsg {
val = cc.Msg.getIntAttr(field)
} else if what == push.ActSub {
val = cc.Sub.getIntAttr(field)
}
if val == 0 {
val = cc.Payload.getIntAttr(field)
}
return val
}
// AndroidVisibilityType defines notification visibility constants
// https://developer.android.com/reference/android/app/Notification.html#visibility
type AndroidVisibilityType string
const (
// AndroidVisibilityUnspecified if unspecified, default to `Visibility.PRIVATE`.
AndroidVisibilityUnspecified AndroidVisibilityType = "VISIBILITY_UNSPECIFIED"
// AndroidVisibilityPrivate show this notification on all lockscreens, but conceals
// sensitive or private information on secure lockscreens.
AndroidVisibilityPrivate AndroidVisibilityType = "PRIVATE"
// AndroidVisibilityPublic show this notification in its entirety on all lockscreens.
AndroidVisibilityPublic AndroidVisibilityType = "PUBLIC"
// AndroidVisibilitySecret do not reveal any part of this notification on a secure lockscreen.
AndroidVisibilitySecret AndroidVisibilityType = "SECRET"
)
// AndroidNotificationPriorityType defines notification priority consumeed by the client
// after it receives the notification. Does not affect FCM sending.
type AndroidNotificationPriorityType string
const (
// If priority is unspecified, notification priority is set to `PRIORITY_DEFAULT`.
AndroidNotificationPriorityUnspecified AndroidNotificationPriorityType = "PRIORITY_UNSPECIFIED"
// Lowest notification priority. Notifications with this `PRIORITY_MIN` might not be
// shown to the user except under special circumstances, such as detailed notification logs.
AndroidNotificationPriorityMin AndroidNotificationPriorityType = "PRIORITY_MIN"
// Lower notification priority. The UI may choose to show the notifications smaller,
// or at a different position in the list, compared with notifications with `PRIORITY_DEFAULT`.
AndroidNotificationPriorityLow AndroidNotificationPriorityType = "PRIORITY_LOW"
// Default notification priority. If the application does not prioritize its own notifications,
// use this value for all notifications.
AndroidNotificationPriorityDefault AndroidNotificationPriorityType = "PRIORITY_DEFAULT"
// Higher notification priority. Use this for more important notifications or alerts.
// The UI may choose to show these notifications larger, or at a different position in the notification
// lists, compared with notifications with `PRIORITY_DEFAULT`.
AndroidNotificationPriorityHigh AndroidNotificationPriorityType = "PRIORITY_HIGH"
// Highest notification priority. Use this for the application's most important items that
// require the user's prompt attention or input.
AndroidNotificationPriorityMax AndroidNotificationPriorityType = "PRIORITY_MAX"
)
// AndroidPriorityType defines the server-side priorities https://goo.gl/GjONJv. It affects how soon
// FCM sends the push.
type AndroidPriorityType string
const (
// Default priority for data messages. Normal priority messages won't open network
// connections on a sleeping device, and their delivery may be delayed to conserve
// the battery. For less time-sensitive messages, such as notifications of new email
// or other data to sync, choose normal delivery priority.
AndroidPriorityNormal AndroidPriorityType = "NORMAL"
// Default priority for notification messages. FCM attempts to deliver high priority
// messages immediately, allowing the FCM service to wake a sleeping device when possible
// and open a network connection to your app server. Apps with instant messaging, chat,
// or voice call alerts, for example, generally need to open a network connection and make
// sure FCM delivers the message to the device without delay. Set high priority if the message
// is time-critical and requires the user's immediate interaction, but beware that setting
// your messages to high priority contributes more to battery drain compared with normal priority messages.
AndroidPriorityHigh AndroidPriorityType = "HIGH"
)
// InterruptionLevelType defines the values for the APNS payload.aps.InterruptionLevel.
type InterruptionLevelType string
const (
// InterruptionLevelPassive is used to indicate that notification be delivered in a passive manner.
InterruptionLevelPassive InterruptionLevelType = "passive"
// InterruptionLevelActive is used to indicate the importance and delivery timing of a notification.
InterruptionLevelActive InterruptionLevelType = "active"
// InterruptionLevelTimeSensitive is used to indicate the importance and delivery timing of a notification.
InterruptionLevelTimeSensitive InterruptionLevelType = "time-sensitive"
// InterruptionLevelCritical is used to indicate the importance and delivery timing of a notification.
// This interruption level requires an approved entitlement from Apple.
// See: https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel/
InterruptionLevelCritical InterruptionLevelType = "critical"
)
const (
// A canonical UUID that identifies the notification. If there is an error sending the notification,
// APNs uses this value to identify the notification to your server.
// The canonical form is 32 lowercase hexadecimal digits, displayed in five groups separated by hyphens
// in the form 8-4-4-4-12. An example UUID is as follows: 123e4567-e89b-12d3-a456-42665544000
// If you omit this header, a new UUID is created by APNs and returned in the response.
HeaderApnsID = "apns-id"
// A UNIX epoch date expressed in seconds (UTC). This header identifies the date when the notification
// is no longer valid and can be discarded.
// If this value is nonzero, APNs stores the notification and tries to deliver it at least once, repeating
// the attempt as needed if it is unable to deliver the notification the first time. If the value is 0,
// APNs treats the notification as if it expires immediately and does not store the notification or attempt
// to redeliver it.
HeaderApnsExpiration = "apns-expiration"
// The priority of the notification. Specify one of the following values:
// 10–Send the push message immediately. Notifications with this priority must trigger an alert, sound,
// or badge on the target device. It is an error to use this priority for a push notification that
// contains only the content-available key.
// 5—Send the push message at a time that takes into account power considerations for the device.
// Notifications with this priority might be grouped and delivered in bursts. They are throttled,
// and in some cases are not delivered.
// If you omit this header, the APNs server sets the priority to 10.
HeaderApnsPriority = "apns-priority"
// The topic of the remote notification, which is typically the bundle ID for your app.
// The certificate you create in your developer account must include the capability for this topic.
// If your certificate includes multiple topics, you must specify a value for this header.
// If you omit this request header and your APNs certificate does not specify multiple topics,
// the APNs server uses the certificate’s Subject as the default topic.
// If you are using a provider token instead of a certificate, you must specify a value for this
// request header. The topic you provide should be provisioned for the your team named in your developer account.
HeaderApnsTopic = "apns-topic"
// Multiple notifications with the same collapse identifier are displayed to the user as a single notification.
// The value of this key must not exceed 64 bytes. For more information, see Quality of Service,
// Store-and-Forward, and Coalesced Notifications.
HeaderApnsCollapseID = "apns-collapse-id"
// The value of this header must accurately reflect the contents of your notification’s payload.
// If there’s a mismatch, or if the header is missing on required systems, APNs may return an error,
// delay the delivery of the notification, or drop it altogether.
HeaderApnsPushType = "apns-push-type"
)
type ApnsPushTypeType string
const (
// Use the alert push type for notifications that trigger a user interaction—for example, an alert, badge, or sound.
// If you set this push type, the apns-topic header field must use your app’s bundle ID as the topic.
// For more information, see Generating a remote notification.
// If the notification requires immediate action from the user, set notification priority to 10; otherwise use 5.
ApnsPushTypeAlert ApnsPushTypeType = "alert"
// Use the background push type for notifications that deliver content in the background, and don’t trigger any user interactions.
// If you set this push type, the apns-topic header field must use your app’s bundle ID as the topic. Always use priority 5.
// Using priority 10 is an error. For more information, see Pushing Background Updates to Your App.
ApnsPushTypeBackground ApnsPushTypeType = "background"
// Use the location push type for notifications that request a user’s location. If you set this push type,
// the apns-topic header field must use your app’s bundle ID with .location-query appended to the end.
// If the location query requires an immediate response from the Location Push Service Extension, set notification
// apns-priority to 10; otherwise, use 5. The location push type supports only token-based authentication.
ApnsPushTypeLocation ApnsPushTypeType = "location"
// Use the voip push type for notifications that provide information about an incoming Voice-over-IP (VoIP) call.
// For more information, see Responding to VoIP Notifications from PushKit.
// If you set this push type, the apns-topic header field must use your app’s bundle ID with .voip appended to the end.
// If you’re using certificate-based authentication, you must also register the certificate for VoIP services.
// The topic is then part of the 1.2.840.113635.100.6.3.4 or 1.2.840.113635.100.6.3.6 extension.
ApnsPushTypeVoip ApnsPushTypeType = "voip"
// Use the fileprovider push type to signal changes to a File Provider extension. If you set this push type,
// the apns-topic header field must use your app’s bundle ID with .pushkit.fileprovider appended to the end.
// For more information, see Using Push Notifications to Signal Changes.
ApnsPushTypeFileprovider ApnsPushTypeType = "fileprovider"
)
// Aps is the APNS payload. See explanation here:
// https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification#2943363
type Aps struct {
Alert *ApsAlert `json:"alert,omitempty"`
Badge int `json:"badge,omitempty"`
Category string `json:"category,omitempty"`
ContentAvailable int `json:"content-available,omitempty"`
InterruptionLevel InterruptionLevelType `json:"interruption-level,omitempty"`
MutableContent int `json:"mutable-content,omitempty"`
RelevanceScore any `json:"relevance-score,omitempty"`
Sound any `json:"sound,omitempty"`
ThreadID string `json:"thread-id,omitempty"`
URLArgs []string `json:"url-args,omitempty"`
}
// ApsAlert is the content of the aps.Alert field.
type ApsAlert struct {
Action string `json:"action,omitempty"`
ActionLocKey string `json:"action-loc-key,omitempty"`
Body string `json:"body,omitempty"`
LaunchImage string `json:"launch-image,omitempty"`
LocArgs []string `json:"loc-args,omitempty"`
LocKey string `json:"loc-key,omitempty"`
Title string `json:"title,omitempty"`
Subtitle string `json:"subtitle,omitempty"`
TitleLocArgs []string `json:"title-loc-args,omitempty"`
TitleLocKey string `json:"title-loc-key,omitempty"`
SummaryArg string `json:"summary-arg,omitempty"`
SummaryArgCount int `json:"summary-arg-count,omitempty"`
}
// FCM error codes
const (
// No more information is available about this error.
ErrorUnspecified = "UNSPECIFIED_ERROR"
// Request parameters were invalid (HTTP error code = 400). An extension of type google.rpc.BadRequest is returned
// to specify which field was invalid.
// Potential causes:
// - Invalid registration: Check the format of the registration token you pass to the server. Make sure it matches
// the registration token the client app receives from registering with Firebase Notifications.
// Do not truncate or add additional characters.
// - Invalid package name: Make sure the message was addressed to a registration token whose package name matches
// the value passed in the request.
// - Message too big: Check that the total size of the payload data included in a message does not exceed FCM limits:
// 4096 bytes for most messages, or 2048 bytes in the case of messages to topics. This includes both the keys and the values.
// - Invalid data key: Check that the payload data does not contain a key (such as from, or gcm, or any value prefixed
// by google) that is used internally by FCM. Note that some words (such as collapse_key) are also used by FCM but are
// allowed in the payload, in which case the payload value will be overridden by the FCM value.
// - Invalid TTL: Check that the value used in ttl is an integer representing a duration in seconds between 0 and
// 2,419,200 (4 weeks).
// - Invalid parameters: Check that the provided parameters have the right name and type.
ErrorInvalidArgument = "INVALID_ARGUMENT"
// App instance was unregistered from FCM (HTTP error code = 404). This usually means that the token used is no
// longer valid and a new one must be used.
// This error can be caused by missing registration tokens, or unregistered tokens.
// - Missing Registration: If the message's target is a token value, check that the request contains a registration token.
// - Not registered: An existing registration token may cease to be valid in a number of scenarios, including:
// - If the client app unregisters with FCM.
// - If the client app is automatically unregistered, which can happen if the user uninstalls the application.
// For example, on iOS, if the APNS Feedback Service reported the APNS token as invalid.
// - If the registration token expires (for example, Google might decide to refresh registration tokens,
// or the APNS token has expired for iOS devices).
// - If the client app is updated but the new version is not configured to receive messages.
// For all these cases, remove this registration token from the app server and stop using it to send messages.
ErrorUnregistered = "UNREGISTERED"
// The authenticated sender ID is different from the sender ID for the registration token (HTTP error code = 403).
// A registration token is tied to a certain group of senders. When a client app registers for FCM, it must specify
// which senders are allowed to send messages. You should use one of those sender IDs when sending messages to
// the client app. If you switch to a different sender, the existing registration tokens won't work.
ErrorSenderIDMismatch = "SENDER_ID_MISMATCH"
// Sending limit exceeded for the message target (HTTP error code = 429). An extension of type google.rpc.QuotaFailure
// is returned to specify which quota got exceeded. This error can be caused by exceeded message rate quota,
// exceeded device message rate quota, or exceeded topic message rate quota.
// - Message rate exceeded: The sending rate of messages is too high. Reduce the number of messages sent and use
// exponential backoff to retry sending.
// - Device message rate exceeded: The rate of messages to a particular device is too high. If an iOS app sends
// messages at a rate exceeding APNs limits, it may receive this error message. Reduce the number of messages
// sent to this device and use exponential backoff to retry sending.
// - Topic message rate exceeded: The rate of messages to subscribers to a particular topic is too high.
// Reduce the number of messages sent for this topic and use exponential backoff to retry sending.
ErrorQuotaExceeded = "QUOTA_EXCEEDED"
// The server is overloaded (HTTP error code = 503). The server couldn't process the request in time. Retry the
// same request, but you must:
// - Honor the Retry-After header if it is included in the response from the FCM Connection Server.
// - Implement exponential back-off in your retry mechanism. (e.g. if you waited one second before the first retry,
// wait at least two second before the next one, then 4 seconds and so on). If you're sending multiple messages,
// delay each one independently by an additional random amount to avoid issuing a new request for all messages
// at the same time. Senders that cause problems risk being denylisted.
ErrorUnavailable = "UNAVAILABLE"
// An unknown internal error occurred (HTTP error code = 500). The server encountered an error while trying to process
// the request. You could retry the same request following the requirements listed in "Timeout" (see row above).
// If the error persists, please contact Firebase support.
ErrorInternal = "INTERNAL"
// APNs certificate or web push auth key was invalid or missing (HTTP error code = 401). A message targeted to an
// iOS device or a web push registration could not be sent. Check the validity of your development and production
// credentials.
ErrorThirdPartyAuth = "THIRD_PARTY_AUTH_ERROR"
)
// APNS error messages
const (
// The collapse identifier exceeds the maximum allowed size (HTTP error code = 400).
ErrorApnsBadCollapseId = "BadCollapseId"
// The specified device token was bad. Verify that the request contains a valid token and that the
// token matches the environment (HTTP error code = 400).
ErrorApnsBadDeviceToken = "BadDeviceToken"
// The apns-expiration value is bad (HTTP error code = 400).
ErrorApnsBadExpirationDate = "BadExpirationDate"
// The apns-id value is bad (HTTP error code = 400).
ErrorApnsBadMessageId = "BadMessageId"
// The apns-priority value is bad (HTTP error code = 400).
ErrorApnsBadPriority = "BadPriority"
// The apns-topic was invalid (HTTP error code = 400).
ErrorApnsBadTopic = "BadTopic"
// The device token does not match the specified topic (HTTP error code = 400).
ErrorApnsDeviceTokenNotForTopic = "DeviceTokenNotForTopic"
// One or more headers were repeated (HTTP error code = 400).
ErrorApnsDuplicateHeaders = "DuplicateHeaders"
// Idle time out (HTTP error code = 400).
ErrorApnsIdleTimeout = "IdleTimeout"
// The device token is not specified in the request :path. Verify that the :path header
// contains the device token (HTTP error code = 400).
ErrorApnsMissingDeviceToken = "MissingDeviceToken"
// The apns-topic header of the request was not specified and was required.
// The apns-topic header is mandatory when the client is connected using a certificate
// that supports multiple topics (HTTP error code = 400).
ErrorApnsMissingTopic = "MissingTopic"
// The message payload was empty (HTTP error code = 400).
ErrorApnsPayloadEmpty = "PayloadEmpty"
// Pushing to this topic is not allowed (HTTP error code = 400).
ErrorApnsTopicDisallowed = "TopicDisallowed"
// The certificate was bad (HTTP error code = 403).
ErrorApnsBadCertificate = "BadCertificate"
// The client certificate was for the wrong environment (HTTP error code = 403).
ErrorApnsBadCertificateEnvironment = "BadCertificateEnvironment"
// The provider token is stale and a new token should be generated (HTTP error code = 403).
ErrorApnsExpiredProviderToken = "ExpiredProviderToken"
// The specified action is not allowed (HTTP error code = 403).
ErrorApnsForbidden = "Forbidden"
// The provider token is not valid or the token signature could not be verified (HTTP error code = 403).
ErrorApnsInvalidProviderToken = "InvalidProviderToken"
// No provider certificate was used to connect to APNs and Authorization header was missing
// or no provider token was specified (HTTP error code = 403).
ErrorApnsMissingProviderToken = "MissingProviderToken"
// The request contained a bad :path value (HTTP error code = 404).
ErrorApnsBadPath = "BadPath"
// The specified :method was not POST (HTTP error code = 405).
ErrorApnsMethodNotAllowed = "MethodNotAllowed"
// The device token is inactive for the specified topic (HTTP error code = 410).
ErrorApnsUnregistered = "Unregistered"
// The message payload was too large. See Creating the Remote Notification Payload
// for details on maximum payload size (HTTP error code = 413).
ErrorApnsPayloadTooLarge = "PayloadTooLarge"
// The provider token is being updated too often (HTTP error code = 429).
ErrorApnsTooManyProviderTokenUpdates = "TooManyProviderTokenUpdates"
// Too many requests were made consecutively to the same device token (HTTP error code = 429).
ErrorApnsTooManyRequests = "TooManyRequests"
// An internal server error occurred (HTTP error code = 500).
ErrorApnsInternalServerError = "InternalServerError"
// The service is unavailable (HTTP error code = 503).
ErrorApnsServiceUnavailable = "ServiceUnavailable"
// The server is shutting down (HTTP error code = 503).
ErrorApnsShutdown = "Shutdown"
)
// GApiError stores a simplified representation of an error returned by a call to Google API.
type GApiError struct {
HttpCode int
// This one is not informative, but can be logged for user consideration.
ErrMessage string
// FCM error code, informative but may be missing.
FcmErrCode string
// Extended error info dependent on the fcmErrCode.
ExtendedInfo string
}
// DecodeGoogleApiError converts very complex googleapi.Error to a bit more manageable structure.
func DecodeGoogleApiError(err error) (decoded *GApiError, errs []error) {
decoded = &GApiError{}
if gerr, ok := err.(*googleapi.Error); ok {
// HTTP status code.
decoded.HttpCode = gerr.Code
decoded.ErrMessage = gerr.Message
for _, errInfo := range gerr.Errors {
decoded.ErrMessage += "; " + errInfo.Reason + "/" + errInfo.Message
}
// Decode the FCM error.
for _, iface := range gerr.Details {
details, ok := iface.(map[string]any)
if !ok {
errs = append(errs, fmt.Errorf("error.Details unrecognized format %T", iface))
continue
}
switch details["@type"] {
case "type.googleapis.com/google.firebase.fcm.v1.FcmError":
if errCode, ok := details["errorCode"].(string); ok {
if decoded.FcmErrCode != "" {
// This has not been observed but FCM is uncler if it can happen.
errs = append(errs, fmt.Errorf("multiple FcmError codes '%s', '%s'", errCode, decoded.FcmErrCode))
} else {
decoded.FcmErrCode = errCode
}
} else {
errs = append(errs, fmt.Errorf("error.Details errorCode is not a string: %T", details["errorCode"]))
}
case "type.googleapis.com/google.rpc.BadRequest":
// dst.fcmErrCode == INVALID_ARGUMENT
if fieldViolations, ok := details["fieldViolations"].([]any); !ok {
errs = append(errs, fmt.Errorf("wrong type of error.Details 'fieldViolations': %T", details["fieldViolations"]))
} else {
var fields []string
for _, violationIface := range fieldViolations {
if violation, ok := violationIface.(map[string]any); !ok {
errs = append(errs, fmt.Errorf("wrong type of error.Details.fieldViolations item: %T", iface))
} else if field, ok := violation["field"].(string); ok && field != "" {
fields = append(fields, field)
} else {
errs = append(errs, fmt.Errorf("error.Details 'fieldViolation' has no 'field': %T, %s", violation["field"], violation["description"]))
}
}
decoded.ExtendedInfo = strings.Join(fields, ",")
}
case "type.googleapis.com/google.rpc.QuotaFailure":
// dst.fcmErrCode == QUOTA_EXCEEDED
// TODO: this error has not been observed, don't know how to handle it.
errs = append(errs, fmt.Errorf("quota exceeded %v", details))
default:
errs = append(errs, fmt.Errorf("unknown error '@type': %v", details))
}
}
} else {
decoded.HttpCode = http.StatusBadRequest
decoded.ErrMessage = err.Error()
errs = append(errs, fmt.Errorf("not googleapi.Error %w", err))
}
if decoded.FcmErrCode == "" {
decoded.FcmErrCode = string(ErrorUnspecified)
}
return
}