feat: send email notifications for missed messages

- New cron job in cluster
- Handle presence and other concerns
- Toggle in app to disable email notifications
- Handles other edge cases such as cooldown period
- Simple html email with simple message

Closes #117
This commit is contained in:
talksik
2026-04-09 14:15:34 -07:00
parent 6fe5af8d8d
commit 51c4c6c822
25 changed files with 725 additions and 29 deletions
+35
View File
@@ -143,6 +143,41 @@ func (rb *RedisBridge) GetPresence(ctx context.Context, channelIDs []string) (ma
return result, nil
}
// GetAllConnectedHumanIDs scans all channel connection hashes in Redis and returns
// the deduplicated set of all humanIDs that have at least one active connection.
func (rb *RedisBridge) GetAllConnectedHumanIDs(ctx context.Context) ([]string, error) {
allHumanIDs := make(map[string]bool)
var cursor uint64
for {
keys, nextCursor, err := rb.client.Scan(ctx, cursor, channelConnsPrefix+"*"+channelConnsSuffix, 100).Result()
if err != nil {
return nil, fmt.Errorf("failed to scan channel keys: %w", err)
}
for _, key := range keys {
members, err := rb.client.HVals(ctx, key).Result()
if err != nil && err != redis.Nil {
continue
}
for _, humanID := range members {
allHumanIDs[humanID] = true
}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
result := make([]string, 0, len(allHumanIDs))
for id := range allHumanIDs {
result = append(result, id)
}
return result, nil
}
// --- Pub/Sub listener (runs in its own goroutine) ---
// Listen subscribes to Redis Pub/Sub and forwards events to the local hub.
+15
View File
@@ -72,7 +72,22 @@ func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
}
// BulkGetPresence implements the gRPC PusherService.
// When channel_ids is empty, returns all connected humanIDs across all channels
// under the key "_all" — useful for checking overall online status.
func (s *Server) BulkGetPresence(ctx context.Context, req *pbpusher.BulkGetPresenceRequest) (*pbpusher.BulkGetPresenceResponse, error) {
// Empty channel_ids → return all connected humans
if len(req.ChannelIds) == 0 {
humanIDs, err := s.bridge.GetAllConnectedHumanIDs(ctx)
if err != nil {
return nil, err
}
return &pbpusher.BulkGetPresenceResponse{
Presences: map[string]*pbpusher.ChannelPresence{
"_all": {HumanIds: humanIDs},
},
}, nil
}
presence, err := s.bridge.GetPresence(ctx, req.ChannelIds)
if err != nil {
return nil, err