205 lines
6.1 KiB
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, ×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, "<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)
|
|
} |