Initial commit: Эфир мессенджер

This commit is contained in:
2026-04-06 14:57:36 +03:00
commit ff93679b6d
50 changed files with 5642 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
package repository
import (
"context"
"time"
"messenger/internal/models"
)
type UserRepository interface {
Create(ctx context.Context, user *models.User) error
FindByID(ctx context.Context, id int64) (*models.User, error)
FindByLogin(ctx context.Context, login string) (*models.User, error)
SearchByLogin(ctx context.Context, query string, limit int) ([]*models.User, error)
UpdateLastSeen(ctx context.Context, userID int64) error
UpdateRole(ctx context.Context, userID int64, role models.UserRole) error
Delete(ctx context.Context, userID int64) error
Exists(ctx context.Context, login string) (bool, error)
}
type ProfileRepository interface {
Create(ctx context.Context, profile *models.Profile) error
FindByUserID(ctx context.Context, userID int64) (*models.Profile, error)
Update(ctx context.Context, profile *models.Profile) error
UpdateAvatar(ctx context.Context, userID int64, avatarURL *string) error
}
type ChatRepository interface {
Create(ctx context.Context, chat *models.Chat) error
FindByID(ctx context.Context, id int64) (*models.Chat, error)
GetUserChats(ctx context.Context, userID int64) ([]*models.Chat, error)
UpdateTitle(ctx context.Context, chatID int64, title string) error
Delete(ctx context.Context, chatID int64) error
// Member operations
AddMember(ctx context.Context, chatID, userID int64, role models.MemberRole) error
RemoveMember(ctx context.Context, chatID, userID int64) error
GetMembers(ctx context.Context, chatID int64) ([]*models.ChatMember, error)
GetMemberRole(ctx context.Context, chatID, userID int64) (*models.MemberRole, error)
UpdateMemberRole(ctx context.Context, chatID, userID int64, role models.MemberRole) error
IsMember(ctx context.Context, chatID, userID int64) (bool, error)
// Private chat specific
FindPrivateChat(ctx context.Context, userID1, userID2 int64) (*models.Chat, error)
}
type MessageRepository interface {
Create(ctx context.Context, message *models.Message) error
FindByID(ctx context.Context, id int64) (*models.Message, error)
GetChatHistory(ctx context.Context, chatID int64, limit int, before time.Time) ([]*models.Message, error)
GetLastMessage(ctx context.Context, chatID int64) (*models.Message, error)
MarkAsRead(ctx context.Context, messageID int64) error
Delete(ctx context.Context, messageID int64) error
Update(ctx context.Context, message *models.Message) error
GetUnreadCount(ctx context.Context, chatID, userID int64) (int, error)
}
type AttachmentRepository interface {
Create(ctx context.Context, attachment *models.Attachment) error
FindByID(ctx context.Context, id int64) (*models.Attachment, error)
UpdateMessageID(ctx context.Context, attachmentID, messageID int64) error
Delete(ctx context.Context, attachmentID int64) error
GetByMessageID(ctx context.Context, messageID int64) (*models.Attachment, error)
}

View 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
}

View 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
}

View 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()
}

View 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
}

View 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
}

View 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
}

View 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
}