package models
import (
"database/sql"
"fmt"
"html"
"html/template"
"regexp"
"strings"
"time"
)
type ChatMessage struct {
ID int `json:"id"`
BoardID int `json:"boardId"`
UserID int `json:"userId"`
Content template.HTML `json:"content"`
ReplyTo int `json:"replyTo"`
Timestamp time.Time `json:"timestamp"`
Username string `json:"username"`
PfpFileID sql.NullInt64 `json:"pfpFileId"`
Mentions []string `json:"mentions"`
}
func CreateChatMessage(db *sql.DB, boardID, userID int, rawContent string, replyTo int) error {
query := "INSERT INTO chat_messages (board_id, user_id, content, reply_to, timestamp) VALUES (?, ?, ?, ?, NOW())"
_, err := db.Exec(query, boardID, userID, rawContent, replyTo)
return err
}
func GetRecentChatMessages(db *sql.DB, boardID int, limit int) ([]ChatMessage, error) {
query := `
SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id
FROM chat_messages cm
JOIN users u ON cm.user_id = u.id
WHERE cm.board_id = ?
ORDER BY cm.timestamp DESC
LIMIT ?`
rows, err := db.Query(query, boardID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var messages []ChatMessage
for rows.Next() {
var msg ChatMessage
var timestampStr string
var rawContent string
err := rows.Scan(&msg.ID, &msg.UserID, &rawContent, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID)
if err != nil {
return nil, err
}
msg.BoardID = boardID
msg.Timestamp, err = time.Parse("2006-01-02 15:04:05", timestampStr)
if err != nil {
msg.Timestamp = time.Time{}
}
msg.Content = renderMarkdown(rawContent)
msg.Mentions = extractMentions(rawContent)
messages = append(messages, msg)
}
return messages, nil
}
func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) {
query := `
SELECT cm.id, cm.board_id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id
FROM chat_messages cm
JOIN users u ON cm.user_id = u.id
WHERE cm.id = ?`
row := db.QueryRow(query, id)
var msg ChatMessage
var timestampStr string
var rawContent string
err := row.Scan(&msg.ID, &msg.BoardID, &msg.UserID, &rawContent, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
msg.Timestamp, err = time.Parse("2006-01-02 15:04:05", timestampStr)
if err != nil {
msg.Timestamp = time.Time{}
}
msg.Content = renderMarkdown(rawContent)
msg.Mentions = extractMentions(rawContent)
return &msg, nil
}
func extractMentions(content string) []string {
re := regexp.MustCompile(`@(\w+)`)
matches := re.FindAllStringSubmatch(content, -1)
mentions := make([]string, len(matches))
for i, match := range matches {
mentions[i] = match[1]
}
return mentions
}
func processInlineMarkdown(line string) string {
line = regexp.MustCompile("`([^`]+)`").ReplaceAllString(line, "$1
")
line = regexp.MustCompile(`\*\*([^\*]+)\*\*`).ReplaceAllString(line, "$1")
line = regexp.MustCompile(`__([^_]+)__`).ReplaceAllString(line, "$1")
line = regexp.MustCompile(`\*([^\*]+)\*`).ReplaceAllString(line, "$1")
line = regexp.MustCompile(`_([^_]+)_`).ReplaceAllString(line, "$1")
line = regexp.MustCompile(`@(\w+)`).ReplaceAllString(line, ` `)
return line
}
func renderMarkdown(content string) template.HTML {
var sb strings.Builder
codeBlockRegex := regexp.MustCompile("(?s)```(\\w*)\n(.*?)\n```")
codeBlocks := make(map[string]string)
codeBlockCounter := 0
contentWithoutCodeBlocks := codeBlockRegex.ReplaceAllStringFunc(content, func(match string) string {
submatches := codeBlockRegex.FindStringSubmatch(match)
lang := submatches[1]
codeContent := submatches[2]
escapedCode := html.EscapeString(codeContent)
var renderedCodeBlock string
if lang != "" {
renderedCodeBlock = fmt.Sprintf("
%s
", lang, escapedCode)
} else {
renderedCodeBlock = fmt.Sprintf("%s
", escapedCode)
}
placeholder := fmt.Sprintf("", codeBlockCounter)
codeBlocks[placeholder] = renderedCodeBlock
codeBlockCounter++
return placeholder
})
lines := strings.Split(contentWithoutCodeBlocks, "\n")
inList := false
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "### ") {
if inList { sb.WriteString("\n"); inList = false }
sb.WriteString("") sb.WriteString(processInlineMarkdown(trimmedLine)) sb.WriteString("
\n") } else { sb.WriteString("\n") } } if inList { sb.WriteString("\n") } finalContent := sb.String() for placeholder, blockHTML := range codeBlocks { finalContent = strings.ReplaceAll(finalContent, placeholder, blockHTML) } finalContent = regexp.MustCompile(`\n{3,}`).ReplaceAllString(finalContent, "\n\n") return template.HTML(finalContent) }