Files
dumpsterChat/internal/message/mentions.go
T
hobokenchicken 613d4b8c6e Complete WebAuthn backend + push notification mention parsing
Backend:
- internal/auth/webauthn.go: full RegisterBegin/Finish, LoginBegin/Finish implementation
  - In-memory challenge store with eviction
  - Credential storage in webauthn_credentials table
  - Session creation on successful passkey login
  - Cookie-based challenge linking for login flow
- internal/message/mentions.go: @mention parsing and push notification dispatch
  - Parses @user, @everyone, @role mentions
  - Skips DND users
  - Async goroutine dispatch for non-blocking
- internal/message/handlers.go: wired mention handler into Create
- cmd/server/main.go: added push handler initialization, pass to message handler
2026-06-28 19:06:03 -04:00

147 lines
3.6 KiB
Go

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
rows, err := m.db.QueryContext(ctx,
`SELECT user_id FROM members WHERE server_id = $1 AND user_id != $2`,
serverID, authorID,
)
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
}
go m.push.SendPush(ctx, userID, payload)
}
}