package message import ( "context" "database/sql" "log/slog" "regexp" "strings" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push" ) var mentionRegex = regexp.MustCompile(`<@([0-9a-f-]+)>`) var everyoneMention = "@everyone" var roleMentionRegex = regexp.MustCompile(`<@&([0-9a-f-]+)>`) // MentionHandler dispatches push notifications for @mentions. type MentionHandler struct { db *sql.DB push *push.Handler logger *slog.Logger } func NewMentionHandler(db *sql.DB, pushHandler *push.Handler, logger *slog.Logger) *MentionHandler { return &MentionHandler{ db: db, push: pushHandler, logger: logger, } } // ParseAndNotify parses message content for mentions and sends push notifications. func (m *MentionHandler) ParseAndNotify(ctx context.Context, channelID, authorID, content string) { // Find individual user mentions userMatches := mentionRegex.FindAllStringSubmatch(content, -1) mentionedUsers := make(map[string]bool) for _, match := range userMatches { if len(match) > 1 { mentionedUsers[match[1]] = true } } // Check for @everyone isEveryone := strings.Contains(content, everyoneMention) // Get channel info for notification var serverID, channelName string err := m.db.QueryRowContext(ctx, `SELECT server_id, name FROM channels WHERE id = $1`, channelID, ).Scan(&serverID, &channelName) if err != nil { m.logger.Error("failed to get channel info for mentions", "error", err) return } // Get author info var authorName string err = m.db.QueryRowContext(ctx, `SELECT COALESCE(display_name, username) FROM users WHERE id = $1`, authorID, ).Scan(&authorName) if err != nil { m.logger.Error("failed to get author info for mentions", "error", err) return } // Truncate content for notification notifContent := content if len(notifContent) > 200 { notifContent = notifContent[:200] + "..." } payload := map[string]interface{}{ "title": authorName + " in #" + channelName, "body": notifContent, "url": "/channels/" + channelID, } if isEveryone { // Send to all server members except those who muted this channel rows, err := m.db.QueryContext(ctx, `SELECT m.user_id FROM members m LEFT JOIN notification_settings ns ON ns.user_id = m.user_id AND ns.channel_id = $3 WHERE m.server_id = $1 AND m.user_id != $2 AND (ns.level IS NULL OR ns.level != 'none')`, serverID, authorID, channelID, ) if err != nil { m.logger.Error("failed to query server members for @everyone", "error", err) return } defer rows.Close() for rows.Next() { var userID string if err := rows.Scan(&userID); err != nil { continue } go m.push.SendPush(ctx, userID, payload) } return } // Check for role mentions roleMatches := roleMentionRegex.FindAllStringSubmatch(content, -1) if len(roleMatches) > 0 { for _, match := range roleMatches { if len(match) > 1 { roleID := match[1] // Get users with this role rows, err := m.db.QueryContext(ctx, `SELECT user_id FROM member_roles WHERE role_id = $1 AND user_id != $2`, roleID, authorID, ) if err != nil { m.logger.Error("failed to query role members", "error", err, "role_id", roleID) continue } for rows.Next() { var userID string if err := rows.Scan(&userID); err != nil { continue } mentionedUsers[userID] = true } rows.Close() } } } // Remove the author from mentions delete(mentionedUsers, authorID) // Send push to individually mentioned users for userID := range mentionedUsers { // Check if user is in DND status var status string err := m.db.QueryRowContext(ctx, `SELECT COALESCE(status, 'online') FROM users WHERE id = $1`, userID, ).Scan(&status) if err != nil { continue } if status == "dnd" { continue } // Check if user muted this channel var level string err = m.db.QueryRowContext(ctx, `SELECT level FROM notification_settings WHERE user_id = $1 AND channel_id = $2`, userID, channelID, ).Scan(&level) if err == nil && level == "none" { continue } go m.push.SendPush(ctx, userID, payload) } }