threadr.lostcave.ddnss.de/models/chat.go

205 lines
6.1 KiB
Go

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, &timestampStr, &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, &timestampStr, &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, "<code>$1</code>")
line = regexp.MustCompile(`\*\*([^\*]+)\*\*`).ReplaceAllString(line, "<strong>$1</strong>")
line = regexp.MustCompile(`__([^_]+)__`).ReplaceAllString(line, "<strong>$1</strong>")
line = regexp.MustCompile(`\*([^\*]+)\*`).ReplaceAllString(line, "<em>$1</em>")
line = regexp.MustCompile(`_([^_]+)_`).ReplaceAllString(line, "<em>$1</em>")
line = regexp.MustCompile(`@(\w+)`).ReplaceAllString(line, `<span class="chat-message-mention">@$1</span>`)
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("<pre><code class=\"language-%s\">%s</code></pre>", lang, escapedCode)
} else {
renderedCodeBlock = fmt.Sprintf("<pre><code>%s</code></pre>", escapedCode)
}
placeholder := fmt.Sprintf("<!--CODEBLOCK_%d-->", 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("</ul>\n"); inList = false }
sb.WriteString("<h3>")
sb.WriteString(processInlineMarkdown(strings.TrimPrefix(trimmedLine, "### ")))
sb.WriteString("</h3>\n")
continue
} else if strings.HasPrefix(trimmedLine, "## ") {
if inList { sb.WriteString("</ul>\n"); inList = false }
sb.WriteString("<h2>")
sb.WriteString(processInlineMarkdown(strings.TrimPrefix(trimmedLine, "## ")))
sb.WriteString("</h2>\n")
continue
} else if strings.HasPrefix(trimmedLine, "# ") {
if inList { sb.WriteString("</ul>\n"); inList = false }
sb.WriteString("<h1>")
sb.WriteString(processInlineMarkdown(strings.TrimPrefix(trimmedLine, "# ")))
sb.WriteString("</h1>\n")
continue
}
if strings.HasPrefix(trimmedLine, "* ") || strings.HasPrefix(trimmedLine, "- ") {
if !inList {
sb.WriteString("<ul>\n")
inList = true
}
listItemContent := strings.TrimPrefix(strings.TrimPrefix(trimmedLine, "* "), "- ")
sb.WriteString("<li>")
sb.WriteString(processInlineMarkdown(listItemContent))
sb.WriteString("</li>\n")
continue
}
if inList {
sb.WriteString("</ul>\n")
inList = false
}
if trimmedLine != "" {
sb.WriteString("<p>")
sb.WriteString(processInlineMarkdown(trimmedLine))
sb.WriteString("</p>\n")
} else {
sb.WriteString("\n")
}
}
if inList {
sb.WriteString("</ul>\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)
}