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, `@$1`) 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(strings.TrimPrefix(trimmedLine, "### "))) sb.WriteString("

\n") continue } else if strings.HasPrefix(trimmedLine, "## ") { if inList { sb.WriteString("\n"); inList = false } sb.WriteString("

") sb.WriteString(processInlineMarkdown(strings.TrimPrefix(trimmedLine, "## "))) sb.WriteString("

\n") continue } else if strings.HasPrefix(trimmedLine, "# ") { if inList { sb.WriteString("\n"); inList = false } sb.WriteString("

") sb.WriteString(processInlineMarkdown(strings.TrimPrefix(trimmedLine, "# "))) sb.WriteString("

\n") continue } if strings.HasPrefix(trimmedLine, "* ") || strings.HasPrefix(trimmedLine, "- ") { if !inList { sb.WriteString("\n") inList = false } if trimmedLine != "" { 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) }