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
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user