feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
cmpp "github.com/bigwhite/gocmpp"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This registry is the single in-process owner of authenticated downstream
|
||||
// sessions. Delivery and recovery code must resolve sessions through it.
|
||||
|
||||
type downstreamConnectionEvent struct {
|
||||
Account string `json:"account"`
|
||||
ConnectionID string `json:"connectionId"`
|
||||
Status string `json:"status"`
|
||||
RemoteIP string `json:"remoteIp,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
ConnectedAt string `json:"connectedAt,omitempty"`
|
||||
ObservedAt string `json:"observedAt,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
}
|
||||
|
||||
type downstreamSession struct {
|
||||
messageID string
|
||||
account string
|
||||
tenantID string
|
||||
applicationID string
|
||||
enterpriseCode string
|
||||
protocol string
|
||||
srcID string
|
||||
phoneNumber string
|
||||
gatewayMsgID uint64
|
||||
remoteIP string
|
||||
connectedAt time.Time
|
||||
connectionID string
|
||||
conn *cmpp.Conn
|
||||
mu *sync.Mutex
|
||||
presence PresenceStore
|
||||
instanceID string
|
||||
report func(*downstreamSession, string, string)
|
||||
deliveryReport func(downstreamDeliveryLifecycleEvent)
|
||||
protocolLog func(protocolLogEvent)
|
||||
}
|
||||
|
||||
var downstreamRegistry = struct {
|
||||
sync.RWMutex
|
||||
byMessageID map[string]*downstreamSession
|
||||
byAccount map[string]*downstreamSession
|
||||
byConn map[*cmpp.Conn]*downstreamSession
|
||||
}{
|
||||
byMessageID: make(map[string]*downstreamSession),
|
||||
byAccount: make(map[string]*downstreamSession),
|
||||
byConn: make(map[*cmpp.Conn]*downstreamSession),
|
||||
}
|
||||
|
||||
func (s Server) handleActivity(_ *cmpp.Response, packet *cmpp.Packet, logger *log.Logger) (bool, error) {
|
||||
session := findSessionByConn(packet.Conn)
|
||||
if session == nil {
|
||||
return true, nil
|
||||
}
|
||||
switch response := packet.Packer.(type) {
|
||||
case *cmpp.CmppActiveTestReqPkt, *cmpp.CmppActiveTestRspPkt:
|
||||
if session.report != nil {
|
||||
go s.reportConnectionOrDisconnect(session, "heartbeat", "")
|
||||
}
|
||||
case *cmpp.Cmpp2DeliverRspPkt:
|
||||
handleDownstreamAcknowledgement(packet.Conn, response.SeqId, response.MsgId, uint32(response.Result), logger)
|
||||
case *cmpp.Cmpp3DeliverRspPkt:
|
||||
handleDownstreamAcknowledgement(packet.Conn, response.SeqId, response.MsgId, response.Result, logger)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s Server) reportConnection(session *downstreamSession, status string, errorMessage string) {
|
||||
_ = s.reportConnectionChecked(session, status, errorMessage)
|
||||
}
|
||||
|
||||
func (s Server) reportConnectionChecked(session *downstreamSession, status string, errorMessage string) error {
|
||||
if session == nil || strings.TrimSpace(session.account) == "" || strings.TrimSpace(session.connectionID) == "" {
|
||||
return nil
|
||||
}
|
||||
event := downstreamConnectionEvent{
|
||||
Account: session.account, ConnectionID: session.connectionID, Status: status,
|
||||
RemoteIP: session.remoteIP, Protocol: session.protocol,
|
||||
ConnectedAt: formatRFC3339Nano(session.connectedAt), ObservedAt: formatRFC3339Nano(time.Now()),
|
||||
ErrorMessage: errorMessage,
|
||||
}
|
||||
if err := s.post(context.Background(), "/gateway/events/inbound/connection", event, nil); err != nil {
|
||||
log.Printf("cmpp inbound connection state callback failed account=%s connection_id=%s status=%s err=%v", session.account, session.connectionID, status, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Server) reportConnectionOrDisconnect(session *downstreamSession, status string, errorMessage string) {
|
||||
if err := s.reportConnectionChecked(session, status, errorMessage); err != nil && status != "disconnected" {
|
||||
_ = s.reportConnectionChecked(session, "disconnected", err.Error())
|
||||
forgetDownstream(session)
|
||||
if session.conn != nil {
|
||||
session.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) handleConnectionClosed(conn *cmpp.Conn) {
|
||||
downstreamSubmitBarrier.Lock()
|
||||
delete(downstreamSubmitBarrier.byConn, conn)
|
||||
downstreamSubmitBarrier.Unlock()
|
||||
session := findSessionByConn(conn)
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
forgetDownstream(session)
|
||||
_ = s.reportConnectionChecked(session, "disconnected", "CMPP client connection closed")
|
||||
}
|
||||
|
||||
func findSessionByConn(conn *cmpp.Conn) *downstreamSession {
|
||||
downstreamRegistry.RLock()
|
||||
defer downstreamRegistry.RUnlock()
|
||||
return downstreamRegistry.byConn[conn]
|
||||
}
|
||||
|
||||
func rememberDownstream(session downstreamSession) {
|
||||
if session.messageID == "" || session.conn == nil {
|
||||
return
|
||||
}
|
||||
session.touchPresence("connected", true, false)
|
||||
downstreamRegistry.Lock()
|
||||
if existing := downstreamRegistry.byMessageID[session.messageID]; existing != nil && existing.conn == session.conn {
|
||||
// A downstream long message returns one SUBMIT_RESP per fragment but is
|
||||
// persisted as one platform message. Keep the first fragment Msg_Id so
|
||||
// online delivery and restart recovery (which persists the first
|
||||
// Sequence_Id) address the same client-side message.
|
||||
session.gatewayMsgID = existing.gatewayMsgID
|
||||
}
|
||||
downstreamRegistry.byMessageID[session.messageID] = &session
|
||||
downstreamRegistry.byConn[session.conn] = &session
|
||||
if session.account != "" {
|
||||
downstreamRegistry.byAccount[session.account] = &session
|
||||
}
|
||||
downstreamRegistry.Unlock()
|
||||
}
|
||||
|
||||
func rememberAccount(session *downstreamSession, maxConnections int) bool {
|
||||
if session == nil || session.account == "" || session.conn == nil {
|
||||
return false
|
||||
}
|
||||
if maxConnections <= 0 {
|
||||
maxConnections = 1
|
||||
}
|
||||
session.touchPresence("connected", false, false)
|
||||
downstreamRegistry.Lock()
|
||||
defer downstreamRegistry.Unlock()
|
||||
active := 0
|
||||
for _, current := range downstreamRegistry.byConn {
|
||||
if current != nil && current.account == session.account {
|
||||
active++
|
||||
}
|
||||
}
|
||||
if active >= maxConnections {
|
||||
return false
|
||||
}
|
||||
downstreamRegistry.byAccount[session.account] = session
|
||||
downstreamRegistry.byConn[session.conn] = session
|
||||
return true
|
||||
}
|
||||
|
||||
func forgetDownstream(session *downstreamSession) {
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
downstreamRegistry.Lock()
|
||||
for messageID, current := range downstreamRegistry.byMessageID {
|
||||
if current != nil && current.conn == session.conn {
|
||||
delete(downstreamRegistry.byMessageID, messageID)
|
||||
}
|
||||
}
|
||||
if session.account != "" {
|
||||
if current := downstreamRegistry.byAccount[session.account]; current == session {
|
||||
delete(downstreamRegistry.byAccount, session.account)
|
||||
}
|
||||
}
|
||||
if current := downstreamRegistry.byConn[session.conn]; current == session {
|
||||
delete(downstreamRegistry.byConn, session.conn)
|
||||
}
|
||||
downstreamRegistry.Unlock()
|
||||
_ = session.removePresence()
|
||||
}
|
||||
|
||||
func onlineAccounts() []string {
|
||||
downstreamRegistry.RLock()
|
||||
defer downstreamRegistry.RUnlock()
|
||||
accounts := make([]string, 0, len(downstreamRegistry.byAccount))
|
||||
for account := range downstreamRegistry.byAccount {
|
||||
if strings.TrimSpace(account) != "" {
|
||||
accounts = append(accounts, account)
|
||||
}
|
||||
}
|
||||
return accounts
|
||||
}
|
||||
|
||||
// DisconnectAccount closes every live downstream CMPP session for an
|
||||
// application account. The normal connection-close callback removes registry
|
||||
// and presence state and reports the disconnect to the API.
|
||||
func DisconnectAccount(account string) int {
|
||||
account = strings.TrimSpace(account)
|
||||
if account == "" {
|
||||
return 0
|
||||
}
|
||||
downstreamRegistry.RLock()
|
||||
sessions := make([]*downstreamSession, 0)
|
||||
for _, session := range downstreamRegistry.byConn {
|
||||
if session != nil && session.account == account && session.conn != nil {
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
}
|
||||
downstreamRegistry.RUnlock()
|
||||
for _, session := range sessions {
|
||||
session.conn.Close()
|
||||
}
|
||||
return len(sessions)
|
||||
}
|
||||
|
||||
func (s Server) gatewayInstanceID() string {
|
||||
if strings.TrimSpace(s.GatewayInstanceID) != "" {
|
||||
return strings.TrimSpace(s.GatewayInstanceID)
|
||||
}
|
||||
return "gateway-1"
|
||||
}
|
||||
|
||||
func (session downstreamSession) touchPresence(state string, includeSubmit bool, includeDeliver bool) {
|
||||
if session.presence == nil || strings.TrimSpace(session.account) == "" {
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
snapshot := DownstreamPresence{
|
||||
Account: strings.TrimSpace(session.account),
|
||||
SrcID: strings.TrimSpace(session.srcID),
|
||||
RemoteIP: strings.TrimSpace(session.remoteIP),
|
||||
GatewayInstanceID: strings.TrimSpace(session.instanceID),
|
||||
State: defaultString(strings.TrimSpace(state), "connected"),
|
||||
ConnectedAt: session.connectedAt,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if includeSubmit {
|
||||
snapshot.LastSubmitAt = now
|
||||
}
|
||||
if includeDeliver {
|
||||
snapshot.LastDeliverAt = now
|
||||
}
|
||||
_ = session.presence.TouchAccount(context.Background(), snapshot)
|
||||
}
|
||||
|
||||
func (session downstreamSession) removePresence() error {
|
||||
if session.presence == nil || strings.TrimSpace(session.account) == "" {
|
||||
return nil
|
||||
}
|
||||
return session.presence.RemoveAccount(context.Background(), strings.TrimSpace(session.account))
|
||||
}
|
||||
Reference in New Issue
Block a user