200 lines
4.9 KiB
Go
200 lines
4.9 KiB
Go
|
package models
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"math"
|
||
|
"sort"
|
||
|
"time"
|
||
|
|
||
|
"git.kirsle.net/apps/barertc/pkg/config"
|
||
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||
|
)
|
||
|
|
||
|
type DirectMessage struct {
|
||
|
MessageID int64
|
||
|
ChannelID string
|
||
|
Username string
|
||
|
Message string
|
||
|
Timestamp int64
|
||
|
}
|
||
|
|
||
|
const DirectMessagePerPage = 20
|
||
|
|
||
|
func (dm DirectMessage) CreateTable() error {
|
||
|
if DB == nil {
|
||
|
return ErrNotInitialized
|
||
|
}
|
||
|
|
||
|
_, err := DB.Exec(`
|
||
|
CREATE TABLE IF NOT EXISTS direct_messages (
|
||
|
message_id INTEGER PRIMARY KEY,
|
||
|
channel_id TEXT,
|
||
|
username TEXT,
|
||
|
message TEXT,
|
||
|
timestamp INTEGER
|
||
|
);
|
||
|
|
||
|
CREATE INDEX IF NOT EXISTS idx_direct_messages_channel_id ON direct_messages(channel_id);
|
||
|
CREATE INDEX IF NOT EXISTS idx_direct_messages_timestamp ON direct_messages(timestamp);
|
||
|
`)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Delete old messages past the retention period.
|
||
|
if days := config.Current.DirectMessageHistory.RetentionDays; days > 0 {
|
||
|
cutoff := time.Now().Add(time.Duration(-days) * 24 * time.Hour)
|
||
|
log.Info("Deleting old DM history past %d days (cutoff: %s)", days, cutoff.Format(time.RFC3339))
|
||
|
_, err := DB.Exec(
|
||
|
"DELETE FROM direct_messages WHERE timestamp < ?",
|
||
|
cutoff.Unix(),
|
||
|
)
|
||
|
if err != nil {
|
||
|
log.Error("Error removing old DMs: %s", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// LogMessage adds a message to the DM history between two users.
|
||
|
func (dm DirectMessage) LogMessage(fromUsername, toUsername string, msg messages.Message) error {
|
||
|
if DB == nil {
|
||
|
return ErrNotInitialized
|
||
|
}
|
||
|
|
||
|
if msg.MessageID == 0 {
|
||
|
return errors.New("message did not have a MessageID")
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
channelID = CreateChannelID(fromUsername, toUsername)
|
||
|
timestamp = time.Now().Unix()
|
||
|
)
|
||
|
|
||
|
_, err := DB.Exec(`
|
||
|
INSERT INTO direct_messages (message_id, channel_id, username, message, timestamp)
|
||
|
VALUES (?, ?, ?, ?, ?)
|
||
|
`, msg.MessageID, channelID, fromUsername, msg.Message, timestamp)
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// TakebackMessage removes a message by its MID from the DM history.
|
||
|
//
|
||
|
// Because the MessageID may have been from a previous chat session, the server can't immediately
|
||
|
// verify the current user had permission to take it back. This function instead will check whether
|
||
|
// a DM history exists sent by this username for that messageID, and if so, returns a
|
||
|
// boolean true that the username/messageID matched which will satisfy the permission check
|
||
|
// in the OnTakeback handler.
|
||
|
func (dm DirectMessage) TakebackMessage(username string, messageID int64, isAdmin bool) (bool, error) {
|
||
|
if DB == nil {
|
||
|
return false, ErrNotInitialized
|
||
|
}
|
||
|
|
||
|
// Does this messageID exist as sent by the user?
|
||
|
if !isAdmin {
|
||
|
var (
|
||
|
row = DB.QueryRow(
|
||
|
"SELECT message_id FROM direct_messages WHERE username = ? AND message_id = ?",
|
||
|
username, messageID,
|
||
|
)
|
||
|
foundMsgID int64
|
||
|
err = row.Scan(&foundMsgID)
|
||
|
)
|
||
|
if err != nil {
|
||
|
return false, errors.New("no such message ID found as owned by that user")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Delete it.
|
||
|
_, err := DB.Exec(
|
||
|
"DELETE FROM direct_messages WHERE message_id = ?",
|
||
|
messageID,
|
||
|
)
|
||
|
|
||
|
// Return that it was successfully validated and deleted.
|
||
|
return err == nil, err
|
||
|
}
|
||
|
|
||
|
// PaginateDirectMessages returns a page of messages, the count of remaining, and an error.
|
||
|
func PaginateDirectMessages(fromUsername, toUsername string, beforeID int64) ([]messages.Message, int, error) {
|
||
|
if DB == nil {
|
||
|
return nil, 0, ErrNotInitialized
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
result = []messages.Message{}
|
||
|
channelID = CreateChannelID(fromUsername, toUsername)
|
||
|
|
||
|
// Compute the remaining messages after finding the final messageID this page.
|
||
|
lastMessageID int64
|
||
|
remaining int
|
||
|
)
|
||
|
|
||
|
if beforeID == 0 {
|
||
|
beforeID = math.MaxInt64
|
||
|
}
|
||
|
|
||
|
rows, err := DB.Query(`
|
||
|
SELECT message_id, username, message, timestamp
|
||
|
FROM direct_messages
|
||
|
WHERE channel_id = ?
|
||
|
AND message_id < ?
|
||
|
ORDER BY message_id DESC
|
||
|
LIMIT ?
|
||
|
`, channelID, beforeID, DirectMessagePerPage)
|
||
|
if err != nil {
|
||
|
return nil, 0, err
|
||
|
}
|
||
|
|
||
|
for rows.Next() {
|
||
|
var row DirectMessage
|
||
|
if err := rows.Scan(
|
||
|
&row.MessageID,
|
||
|
&row.Username,
|
||
|
&row.Message,
|
||
|
&row.Timestamp,
|
||
|
); err != nil {
|
||
|
return nil, 0, err
|
||
|
}
|
||
|
|
||
|
msg := messages.Message{
|
||
|
MessageID: row.MessageID,
|
||
|
Username: row.Username,
|
||
|
Message: row.Message,
|
||
|
Timestamp: time.Unix(row.Timestamp, 0).Format(time.RFC3339),
|
||
|
}
|
||
|
result = append(result, msg)
|
||
|
lastMessageID = msg.MessageID
|
||
|
}
|
||
|
|
||
|
// Get a count of the remaining messages.
|
||
|
row := DB.QueryRow(`
|
||
|
SELECT COUNT(message_id)
|
||
|
FROM direct_messages
|
||
|
WHERE channel_id = ?
|
||
|
AND message_id < ?
|
||
|
`, channelID, lastMessageID)
|
||
|
if err := row.Scan(&remaining); err != nil {
|
||
|
return nil, 0, err
|
||
|
}
|
||
|
|
||
|
return result, remaining, nil
|
||
|
}
|
||
|
|
||
|
// CreateChannelID returns a deterministic channel ID for a direct message conversation.
|
||
|
//
|
||
|
// The usernames (passed in any order) are sorted alphabetically and composed into the channel ID.
|
||
|
func CreateChannelID(fromUsername, toUsername string) string {
|
||
|
var parts = []string{fromUsername, toUsername}
|
||
|
sort.Strings(parts)
|
||
|
return fmt.Sprintf(
|
||
|
"@%s:@%s",
|
||
|
parts[0],
|
||
|
parts[1],
|
||
|
)
|
||
|
}
|