Initial commit: Эфир мессенджер
This commit is contained in:
133
internal/repository/sqlite/attachment_repo.go
Normal file
133
internal/repository/sqlite/attachment_repo.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"messenger/internal/models"
|
||||
)
|
||||
|
||||
type AttachmentRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewAttachmentRepository(db *DB) *AttachmentRepository {
|
||||
return &AttachmentRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *AttachmentRepository) Create(ctx context.Context, attachment *models.Attachment) error {
|
||||
query := `
|
||||
INSERT INTO attachments (message_id, file_name, file_size, storage_path, mime_type, uploaded_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
attachment.MessageID, attachment.FileName, attachment.FileSize,
|
||||
attachment.StoragePath, attachment.MimeType, attachment.UploadedAt,
|
||||
).Scan(&attachment.ID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create attachment: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AttachmentRepository) FindByID(ctx context.Context, id int64) (*models.Attachment, error) {
|
||||
query := `
|
||||
SELECT id, message_id, file_name, file_size, storage_path, mime_type, uploaded_at
|
||||
FROM attachments
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
var attachment models.Attachment
|
||||
var messageID sql.NullInt64
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&attachment.ID, &messageID, &attachment.FileName, &attachment.FileSize,
|
||||
&attachment.StoragePath, &attachment.MimeType, &attachment.UploadedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find attachment by id: %w", err)
|
||||
}
|
||||
|
||||
if messageID.Valid {
|
||||
attachment.MessageID = &messageID.Int64
|
||||
}
|
||||
|
||||
return &attachment, nil
|
||||
}
|
||||
|
||||
func (r *AttachmentRepository) UpdateMessageID(ctx context.Context, attachmentID, messageID int64) error {
|
||||
query := `UPDATE attachments SET message_id = ? WHERE id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, messageID, attachmentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update attachment message_id: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("attachment not found: %d", attachmentID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AttachmentRepository) Delete(ctx context.Context, attachmentID int64) error {
|
||||
query := `DELETE FROM attachments WHERE id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, attachmentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete attachment: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("attachment not found: %d", attachmentID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AttachmentRepository) GetByMessageID(ctx context.Context, messageID int64) (*models.Attachment, error) {
|
||||
query := `
|
||||
SELECT id, message_id, file_name, file_size, storage_path, mime_type, uploaded_at
|
||||
FROM attachments
|
||||
WHERE message_id = ?
|
||||
`
|
||||
|
||||
var attachment models.Attachment
|
||||
var msgID sql.NullInt64
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, messageID).Scan(
|
||||
&attachment.ID, &msgID, &attachment.FileName, &attachment.FileSize,
|
||||
&attachment.StoragePath, &attachment.MimeType, &attachment.UploadedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attachment by message_id: %w", err)
|
||||
}
|
||||
|
||||
if msgID.Valid {
|
||||
attachment.MessageID = &msgID.Int64
|
||||
}
|
||||
|
||||
return &attachment, nil
|
||||
}
|
||||
261
internal/repository/sqlite/chat_repo.go
Normal file
261
internal/repository/sqlite/chat_repo.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"messenger/internal/models"
|
||||
)
|
||||
|
||||
type ChatRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewChatRepository(db *DB) *ChatRepository {
|
||||
return &ChatRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *ChatRepository) Create(ctx context.Context, chat *models.Chat) error {
|
||||
query := `
|
||||
INSERT INTO chats (type, title, created_at)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, chat.Type, chat.Title, chat.CreatedAt).Scan(&chat.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create chat: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) FindByID(ctx context.Context, id int64) (*models.Chat, error) {
|
||||
query := `
|
||||
SELECT id, type, title, created_at
|
||||
FROM chats
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
var chat models.Chat
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(&chat.ID, &chat.Type, &chat.Title, &chat.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find chat by id: %w", err)
|
||||
}
|
||||
|
||||
return &chat, nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) GetUserChats(ctx context.Context, userID int64) ([]*models.Chat, error) {
|
||||
query := `
|
||||
SELECT c.id, c.type, c.title, c.created_at
|
||||
FROM chats c
|
||||
INNER JOIN chat_members cm ON c.id = cm.chat_id
|
||||
WHERE cm.user_id = ?
|
||||
ORDER BY c.created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user chats: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var chats []*models.Chat
|
||||
for rows.Next() {
|
||||
var chat models.Chat
|
||||
err := rows.Scan(&chat.ID, &chat.Type, &chat.Title, &chat.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan chat: %w", err)
|
||||
}
|
||||
chats = append(chats, &chat)
|
||||
}
|
||||
|
||||
return chats, nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) UpdateTitle(ctx context.Context, chatID int64, title string) error {
|
||||
query := `UPDATE chats SET title = ? WHERE id = ? AND type = 'group'`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, title, chatID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update chat title: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("chat not found or not a group: %d", chatID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) Delete(ctx context.Context, chatID int64) error {
|
||||
query := `DELETE FROM chats WHERE id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, chatID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete chat: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("chat not found: %d", chatID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) AddMember(ctx context.Context, chatID, userID int64, role models.MemberRole) error {
|
||||
query := `
|
||||
INSERT INTO chat_members (chat_id, user_id, role, joined_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query, chatID, userID, role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add member: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) RemoveMember(ctx context.Context, chatID, userID int64) error {
|
||||
query := `DELETE FROM chat_members WHERE chat_id = ? AND user_id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, chatID, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove member: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("member not found in chat")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) GetMembers(ctx context.Context, chatID int64) ([]*models.ChatMember, error) {
|
||||
query := `
|
||||
SELECT chat_id, user_id, role, joined_at
|
||||
FROM chat_members
|
||||
WHERE chat_id = ?
|
||||
ORDER BY joined_at ASC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, chatID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get members: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var members []*models.ChatMember
|
||||
for rows.Next() {
|
||||
var member models.ChatMember
|
||||
err := rows.Scan(&member.ChatID, &member.UserID, &member.Role, &member.JoinedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan member: %w", err)
|
||||
}
|
||||
members = append(members, &member)
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) GetMemberRole(ctx context.Context, chatID, userID int64) (*models.MemberRole, error) {
|
||||
query := `SELECT role FROM chat_members WHERE chat_id = ? AND user_id = ?`
|
||||
|
||||
var role models.MemberRole
|
||||
err := r.db.QueryRowContext(ctx, query, chatID, userID).Scan(&role)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get member role: %w", err)
|
||||
}
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) UpdateMemberRole(ctx context.Context, chatID, userID int64, role models.MemberRole) error {
|
||||
query := `UPDATE chat_members SET role = ? WHERE chat_id = ? AND user_id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, role, chatID, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update member role: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("member not found in chat")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) IsMember(ctx context.Context, chatID, userID int64) (bool, error) {
|
||||
query := `SELECT EXISTS(SELECT 1 FROM chat_members WHERE chat_id = ? AND user_id = ?)`
|
||||
|
||||
var exists bool
|
||||
err := r.db.QueryRowContext(ctx, query, chatID, userID).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check membership: %w", err)
|
||||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (r *ChatRepository) FindPrivateChat(ctx context.Context, userID1, userID2 int64) (*models.Chat, error) {
|
||||
query := `
|
||||
SELECT c.id, c.type, c.title, c.created_at
|
||||
FROM chats c
|
||||
INNER JOIN chat_members cm1 ON c.id = cm1.chat_id
|
||||
INNER JOIN chat_members cm2 ON c.id = cm2.chat_id
|
||||
WHERE c.type = 'private'
|
||||
AND cm1.user_id = ?
|
||||
AND cm2.user_id = ?
|
||||
AND c.id IN (
|
||||
SELECT chat_id
|
||||
FROM chat_members
|
||||
WHERE user_id IN (?, ?)
|
||||
GROUP BY chat_id
|
||||
HAVING COUNT(DISTINCT user_id) = 2
|
||||
)
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
var chat models.Chat
|
||||
err := r.db.QueryRowContext(ctx, query, userID1, userID2, userID1, userID2).Scan(
|
||||
&chat.ID, &chat.Type, &chat.Title, &chat.CreatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find private chat: %w", err)
|
||||
}
|
||||
|
||||
return &chat, nil
|
||||
}
|
||||
52
internal/repository/sqlite/db.go
Normal file
52
internal/repository/sqlite/db.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"messenger/internal/pkg/logger"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func NewDB(dbPath string) (*DB, error) {
|
||||
// Подключение к SQLite с оптимизациями
|
||||
db, err := sql.Open("sqlite", fmt.Sprintf("%s?_journal=WAL&_foreign_keys=on&_busy_timeout=5000", dbPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// Настройка пула соединений
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(10)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
// Проверка соединения
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
// Автоматически выполняем миграции
|
||||
if err := RunMigrations(db, "./migrations"); err != nil {
|
||||
logger.Error("Failed to run migrations", "error", err)
|
||||
// Не возвращаем ошибку, продолжаем работу
|
||||
}
|
||||
|
||||
logger.Info("SQLite database connected", "path", dbPath)
|
||||
|
||||
return &DB{DB: db}, nil
|
||||
}
|
||||
|
||||
func (db *DB) Close() error {
|
||||
logger.Info("Closing SQLite database connection")
|
||||
return db.DB.Close()
|
||||
}
|
||||
|
||||
// BeginTx начинает транзакцию
|
||||
func (db *DB) BeginTx() (*sql.Tx, error) {
|
||||
return db.DB.Begin()
|
||||
}
|
||||
268
internal/repository/sqlite/message_repo.go
Normal file
268
internal/repository/sqlite/message_repo.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"messenger/internal/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MessageRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewMessageRepository(db *DB) *MessageRepository {
|
||||
return &MessageRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *MessageRepository) Create(ctx context.Context, message *models.Message) error {
|
||||
query := `
|
||||
INSERT INTO messages (chat_id, sender_id, encrypted_body, attachment_id, is_read, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
message.ChatID, message.SenderID, message.EncryptedBody,
|
||||
message.AttachmentID, message.IsRead, message.CreatedAt,
|
||||
).Scan(&message.ID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create message: %w", err)
|
||||
}
|
||||
|
||||
// Если есть attachment, обновляем его message_id
|
||||
if message.AttachmentID != nil {
|
||||
updateQuery := `UPDATE attachments SET message_id = ? WHERE id = ?`
|
||||
_, err = r.db.ExecContext(ctx, updateQuery, message.ID, *message.AttachmentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to link attachment to message: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MessageRepository) FindByID(ctx context.Context, id int64) (*models.Message, error) {
|
||||
query := `
|
||||
SELECT id, chat_id, sender_id, encrypted_body, attachment_id, is_read, created_at
|
||||
FROM messages
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
var message models.Message
|
||||
var attachmentID sql.NullInt64
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&message.ID, &message.ChatID, &message.SenderID, &message.EncryptedBody,
|
||||
&attachmentID, &message.IsRead, &message.CreatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find message by id: %w", err)
|
||||
}
|
||||
|
||||
if attachmentID.Valid {
|
||||
message.AttachmentID = &attachmentID.Int64
|
||||
}
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
func (r *MessageRepository) GetChatHistory(ctx context.Context, chatID int64, limit int, before time.Time) ([]*models.Message, error) {
|
||||
var query string
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
|
||||
// Если before не zero time, используем пагинацию
|
||||
if !before.IsZero() {
|
||||
query = `
|
||||
SELECT id, chat_id, sender_id, encrypted_body, attachment_id, is_read, created_at
|
||||
FROM messages
|
||||
WHERE chat_id = ? AND created_at < ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`
|
||||
rows, err = r.db.QueryContext(ctx, query, chatID, before, limit)
|
||||
} else {
|
||||
query = `
|
||||
SELECT id, chat_id, sender_id, encrypted_body, attachment_id, is_read, created_at
|
||||
FROM messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`
|
||||
rows, err = r.db.QueryContext(ctx, query, chatID, limit)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chat history: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var messages []*models.Message
|
||||
for rows.Next() {
|
||||
var message models.Message
|
||||
var attachmentID sql.NullInt64
|
||||
|
||||
err := rows.Scan(
|
||||
&message.ID, &message.ChatID, &message.SenderID, &message.EncryptedBody,
|
||||
&attachmentID, &message.IsRead, &message.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan message: %w", err)
|
||||
}
|
||||
|
||||
if attachmentID.Valid {
|
||||
message.AttachmentID = &attachmentID.Int64
|
||||
}
|
||||
|
||||
messages = append(messages, &message)
|
||||
}
|
||||
|
||||
// Переворачиваем, чтобы получить в хронологическом порядке
|
||||
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
|
||||
messages[i], messages[j] = messages[j], messages[i]
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (r *MessageRepository) GetLastMessage(ctx context.Context, chatID int64) (*models.Message, error) {
|
||||
query := `
|
||||
SELECT id, chat_id, sender_id, encrypted_body, attachment_id, is_read, created_at
|
||||
FROM messages
|
||||
WHERE chat_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
var message models.Message
|
||||
var attachmentID sql.NullInt64
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, chatID).Scan(
|
||||
&message.ID, &message.ChatID, &message.SenderID, &message.EncryptedBody,
|
||||
&attachmentID, &message.IsRead, &message.CreatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get last message: %w", err)
|
||||
}
|
||||
|
||||
if attachmentID.Valid {
|
||||
message.AttachmentID = &attachmentID.Int64
|
||||
}
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
func (r *MessageRepository) MarkAsRead(ctx context.Context, messageID int64) error {
|
||||
query := `UPDATE messages SET is_read = 1 WHERE id = ? AND is_read = 0`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, messageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark message as read: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
// Сообщение уже прочитано или не найдено - не ошибка
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MessageRepository) Delete(ctx context.Context, messageID int64) error {
|
||||
// Сначала получаем attachment_id, чтобы потом удалить файл
|
||||
var attachmentID sql.NullInt64
|
||||
query := `SELECT attachment_id FROM messages WHERE id = ?`
|
||||
err := r.db.QueryRowContext(ctx, query, messageID).Scan(&attachmentID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return fmt.Errorf("failed to get attachment info: %w", err)
|
||||
}
|
||||
|
||||
// Удаляем сообщение (attachment останется, но без связи с сообщением)
|
||||
deleteQuery := `DELETE FROM messages WHERE id = ?`
|
||||
result, err := r.db.ExecContext(ctx, deleteQuery, messageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete message: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("message not found: %d", messageID)
|
||||
}
|
||||
|
||||
// Если был attachment, обновляем его message_id на NULL
|
||||
if attachmentID.Valid {
|
||||
updateQuery := `UPDATE attachments SET message_id = NULL WHERE id = ?`
|
||||
_, err = r.db.ExecContext(ctx, updateQuery, attachmentID.Int64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unlink attachment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MessageRepository) Update(ctx context.Context, message *models.Message) error {
|
||||
query := `
|
||||
UPDATE messages
|
||||
SET encrypted_body = ?, attachment_id = ?, is_read = ?
|
||||
WHERE id = ? AND sender_id = ?
|
||||
`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query,
|
||||
message.EncryptedBody, message.AttachmentID, message.IsRead,
|
||||
message.ID, message.SenderID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update message: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("message not found or not owned by user: %d", message.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MessageRepository) GetUnreadCount(ctx context.Context, chatID, userID int64) (int, error) {
|
||||
query := `
|
||||
SELECT COUNT(*)
|
||||
FROM messages m
|
||||
WHERE m.chat_id = ?
|
||||
AND m.sender_id != ?
|
||||
AND m.is_read = 0
|
||||
`
|
||||
|
||||
var count int
|
||||
err := r.db.QueryRowContext(ctx, query, chatID, userID).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get unread count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
46
internal/repository/sqlite/migrate.go
Normal file
46
internal/repository/sqlite/migrate.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func RunMigrations(db *sql.DB, migrationsPath string) error {
|
||||
// Находим все .up.sql файлы
|
||||
files, err := filepath.Glob(filepath.Join(migrationsPath, "*.up.sql"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find migrations: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fmt.Printf("Applying migration: %s\n", file)
|
||||
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migration file %s: %w", file, err)
|
||||
}
|
||||
|
||||
// Разделяем SQL statements по точке с запятой
|
||||
statements := strings.Split(string(content), ";")
|
||||
|
||||
for _, stmt := range statements {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
if stmt == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Выполняем SQL
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
// Игнорируем ошибку "table already exists"
|
||||
if !strings.Contains(err.Error(), "already exists") {
|
||||
return fmt.Errorf("failed to execute migration %s: %w\nSQL: %s", file, err, stmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
99
internal/repository/sqlite/profile_repo.go
Normal file
99
internal/repository/sqlite/profile_repo.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"messenger/internal/models"
|
||||
)
|
||||
|
||||
type ProfileRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewProfileRepository(db *DB) *ProfileRepository {
|
||||
return &ProfileRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *ProfileRepository) Create(ctx context.Context, profile *models.Profile) error {
|
||||
query := `
|
||||
INSERT INTO profiles (user_id, display_name, bio, avatar_url)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query,
|
||||
profile.UserID, profile.DisplayName, profile.Bio, profile.AvatarURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create profile: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProfileRepository) FindByUserID(ctx context.Context, userID int64) (*models.Profile, error) {
|
||||
query := `
|
||||
SELECT user_id, display_name, bio, avatar_url
|
||||
FROM profiles
|
||||
WHERE user_id = ?
|
||||
`
|
||||
|
||||
var profile models.Profile
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, userID).Scan(
|
||||
&profile.UserID, &profile.DisplayName, &profile.Bio, &profile.AvatarURL,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find profile by user_id: %w", err)
|
||||
}
|
||||
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func (r *ProfileRepository) Update(ctx context.Context, profile *models.Profile) error {
|
||||
query := `
|
||||
UPDATE profiles
|
||||
SET display_name = COALESCE(?, display_name),
|
||||
bio = COALESCE(?, bio)
|
||||
WHERE user_id = ?
|
||||
`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, profile.DisplayName, profile.Bio, profile.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update profile: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("profile not found for user: %d", profile.UserID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProfileRepository) UpdateAvatar(ctx context.Context, userID int64, avatarURL *string) error {
|
||||
query := `UPDATE profiles SET avatar_url = ? WHERE user_id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, avatarURL, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update avatar: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("profile not found for user: %d", userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
197
internal/repository/sqlite/user_repo.go
Normal file
197
internal/repository/sqlite/user_repo.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"messenger/internal/models"
|
||||
//"time"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepository) Create(ctx context.Context, user *models.User) error {
|
||||
query := `
|
||||
INSERT INTO users (login, password_hash, role, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, user.Login, user.PasswordHash, user.Role, user.CreatedAt).Scan(&user.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByID(ctx context.Context, id int64) (*models.User, error) {
|
||||
query := `
|
||||
SELECT id, login, password_hash, role, last_seen, created_at
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
var user models.User
|
||||
var lastSeen sql.NullTime
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&user.ID, &user.Login, &user.PasswordHash, &user.Role,
|
||||
&lastSeen, &user.CreatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find user by id: %w", err)
|
||||
}
|
||||
|
||||
if lastSeen.Valid {
|
||||
user.LastSeen = &lastSeen.Time
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByLogin(ctx context.Context, login string) (*models.User, error) {
|
||||
query := `
|
||||
SELECT id, login, password_hash, role, last_seen, created_at
|
||||
FROM users
|
||||
WHERE login = ?
|
||||
`
|
||||
|
||||
var user models.User
|
||||
var lastSeen sql.NullTime
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, login).Scan(
|
||||
&user.ID, &user.Login, &user.PasswordHash, &user.Role,
|
||||
&lastSeen, &user.CreatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find user by login: %w", err)
|
||||
}
|
||||
|
||||
if lastSeen.Valid {
|
||||
user.LastSeen = &lastSeen.Time
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) SearchByLogin(ctx context.Context, query string, limit int) ([]*models.User, error) {
|
||||
sqlQuery := `
|
||||
SELECT id, login, role, last_seen, created_at
|
||||
FROM users
|
||||
WHERE login LIKE ? || '%'
|
||||
ORDER BY login
|
||||
LIMIT ?
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, sqlQuery, query, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search users: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []*models.User
|
||||
for rows.Next() {
|
||||
var user models.User
|
||||
var lastSeen sql.NullTime
|
||||
|
||||
err := rows.Scan(&user.ID, &user.Login, &user.Role, &lastSeen, &user.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan user: %w", err)
|
||||
}
|
||||
|
||||
if lastSeen.Valid {
|
||||
user.LastSeen = &lastSeen.Time
|
||||
}
|
||||
|
||||
users = append(users, &user)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateLastSeen(ctx context.Context, userID int64) error {
|
||||
query := `UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update last_seen: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("user not found: %d", userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateRole(ctx context.Context, userID int64, role models.UserRole) error {
|
||||
query := `UPDATE users SET role = ? WHERE id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, role, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update role: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("user not found: %d", userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) Delete(ctx context.Context, userID int64) error {
|
||||
query := `DELETE FROM users WHERE id = ?`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("user not found: %d", userID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) Exists(ctx context.Context, login string) (bool, error) {
|
||||
query := `SELECT EXISTS(SELECT 1 FROM users WHERE login = ?)`
|
||||
|
||||
var exists bool
|
||||
err := r.db.QueryRowContext(ctx, query, login).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check user existence: %w", err)
|
||||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
Reference in New Issue
Block a user