mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
feat: add attachment persistence and link on chat_send (High #3)
- Add attachment_queries.go with GetAttachmentByID, LinkAttachmentsToMessage, and GetAttachmentsByMessageIDs - Wire attachment linking in handleChatSend with ATTACH_FILES permission check - Include linked attachments in chat_message WS broadcast payload - Wire attachment batch-fetch into GetMessagesForAPI for REST responses - Add attachments table to channel handler test schema
This commit is contained in:
@@ -109,6 +109,15 @@ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
|
||||
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
message_id INTEGER REFERENCES messages(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
stored_as TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS reactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Attachment represents a row in the attachments table.
|
||||
type Attachment struct {
|
||||
ID string
|
||||
MessageID *int64
|
||||
Filename string
|
||||
StoredAs string
|
||||
MimeType string
|
||||
Size int64
|
||||
UploadedAt string
|
||||
}
|
||||
|
||||
// GetAttachmentByID returns the attachment with the given ID, or nil if not found.
|
||||
func (d *DB) GetAttachmentByID(id string) (*Attachment, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT id, message_id, filename, stored_as, mime_type, size, uploaded_at
|
||||
FROM attachments WHERE id = ?`, id,
|
||||
)
|
||||
a := &Attachment{}
|
||||
err := row.Scan(&a.ID, &a.MessageID, &a.Filename, &a.StoredAs, &a.MimeType, &a.Size, &a.UploadedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAttachmentByID: %w", err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// LinkAttachmentsToMessage sets message_id on attachments that are currently
|
||||
// unlinked (message_id IS NULL). Returns the number of rows updated.
|
||||
// Uses WHERE message_id IS NULL to prevent double-linking in a race.
|
||||
func (d *DB) LinkAttachmentsToMessage(messageID int64, attachmentIDs []string) (int64, error) {
|
||||
if len(attachmentIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
placeholders := make([]string, len(attachmentIDs))
|
||||
args := make([]any, 0, len(attachmentIDs)+1)
|
||||
args = append(args, messageID)
|
||||
for i, id := range attachmentIDs {
|
||||
placeholders[i] = "?"
|
||||
args = append(args, id)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
`UPDATE attachments SET message_id = ? WHERE id IN (%s) AND message_id IS NULL`,
|
||||
strings.Join(placeholders, ","),
|
||||
)
|
||||
res, err := d.sqlDB.Exec(query, args...)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("LinkAttachmentsToMessage: %w", err)
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// GetAttachmentsByMessageIDs returns attachments grouped by message ID.
|
||||
func (d *DB) GetAttachmentsByMessageIDs(msgIDs []int64) (map[int64][]AttachmentInfo, error) {
|
||||
if len(msgIDs) == 0 {
|
||||
return map[int64][]AttachmentInfo{}, nil
|
||||
}
|
||||
|
||||
placeholders := make([]string, len(msgIDs))
|
||||
args := make([]any, len(msgIDs))
|
||||
for i, id := range msgIDs {
|
||||
placeholders[i] = "?"
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
`SELECT id, message_id, filename, size, mime_type
|
||||
FROM attachments WHERE message_id IN (%s)`,
|
||||
strings.Join(placeholders, ","),
|
||||
)
|
||||
rows, err := d.sqlDB.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAttachmentsByMessageIDs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[int64][]AttachmentInfo)
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var msgID int64
|
||||
var ai AttachmentInfo
|
||||
if scanErr := rows.Scan(&id, &msgID, &ai.Filename, &ai.Size, &ai.Mime); scanErr != nil {
|
||||
return nil, fmt.Errorf("GetAttachmentsByMessageIDs scan: %w", scanErr)
|
||||
}
|
||||
ai.ID = id
|
||||
ai.URL = "/api/files/" + id
|
||||
result[msgID] = append(result[msgID], ai)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -305,6 +305,17 @@ func (d *DB) GetMessagesForAPI(channelID, before int64, limit int, requestingUse
|
||||
}
|
||||
}
|
||||
|
||||
// Batch-fetch attachments for all message IDs.
|
||||
attMap, err := d.GetAttachmentsByMessageIDs(msgIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetMessagesForAPI attachments: %w", err)
|
||||
}
|
||||
for i := range msgs {
|
||||
if a, ok := attMap[msgs[i].ID]; ok {
|
||||
msgs[i].Attachments = a
|
||||
}
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
|
||||
+29
-2
@@ -171,7 +171,7 @@ func (h *Hub) handleChatSend(c *Client, reqID string, payload json.RawMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
// Persist.
|
||||
// Persist message.
|
||||
msgID, err := h.db.CreateMessage(channelID, c.userID, content, p.ReplyTo)
|
||||
if err != nil {
|
||||
slog.Error("ws handleChatSend CreateMessage", "err", err)
|
||||
@@ -179,6 +179,33 @@ func (h *Hub) handleChatSend(c *Client, reqID string, payload json.RawMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
// Link attachments if provided.
|
||||
var attachments []map[string]any
|
||||
if len(p.Attachments) > 0 {
|
||||
if !h.hasChannelPerm(c, channelID, permissions.AttachFiles) {
|
||||
c.sendMsg(buildErrorMsg("FORBIDDEN", "missing ATTACH_FILES permission"))
|
||||
return
|
||||
}
|
||||
linked, linkErr := h.db.LinkAttachmentsToMessage(msgID, p.Attachments)
|
||||
if linkErr != nil {
|
||||
slog.Error("ws handleChatSend LinkAttachments", "err", linkErr)
|
||||
}
|
||||
if linked > 0 {
|
||||
attMap, attErr := h.db.GetAttachmentsByMessageIDs([]int64{msgID})
|
||||
if attErr == nil {
|
||||
for _, ai := range attMap[msgID] {
|
||||
attachments = append(attachments, map[string]any{
|
||||
"id": ai.ID,
|
||||
"filename": ai.Filename,
|
||||
"size": ai.Size,
|
||||
"mime": ai.Mime,
|
||||
"url": ai.URL,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve to get timestamp.
|
||||
msg, err := h.db.GetMessage(msgID)
|
||||
if err != nil || msg == nil {
|
||||
@@ -200,7 +227,7 @@ func (h *Hub) handleChatSend(c *Client, reqID string, payload json.RawMessage) {
|
||||
c.sendMsg(buildChatSendOK(reqID, msgID, msg.Timestamp))
|
||||
|
||||
// Broadcast to channel.
|
||||
broadcast := buildChatMessage(msgID, channelID, c.userID, username, avatar, content, msg.Timestamp, p.ReplyTo, nil)
|
||||
broadcast := buildChatMessage(msgID, channelID, c.userID, username, avatar, content, msg.Timestamp, p.ReplyTo, attachments)
|
||||
h.BroadcastToChannel(channelID, broadcast)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user