gohorsejobs/backend/internal/services/chat_service.go
Tiago Yamamoto 841b1d780c feat: Email System, Avatar Upload, Email Templates UI, and Public Job Posting
- Backend: Email producer (LavinMQ), EmailService interface
- Backend: CRUD API for email_templates and email_settings
- Backend: avatar_url field in users table + UpdateMyProfile support
- Backend: StorageService for pre-signed URLs
- NestJS: Email consumer with Nodemailer and Handlebars
- Frontend: Email Templates admin pages (list/edit)
- Frontend: Updated profileApi.uploadAvatar with pre-signed URL flow
- Frontend: New /post-job public page (company registration + job creation wizard)
- Migrations: 027_create_email_system.sql, 028_add_avatar_url_to_users.sql
2025-12-26 12:21:34 -03:00

156 lines
4.4 KiB
Go

package services
import (
"context"
"database/sql"
"fmt"
"time"
)
type ChatService struct {
DB *sql.DB
Appwrite *AppwriteService
}
func NewChatService(db *sql.DB, appwrite *AppwriteService) *ChatService {
return &ChatService{
DB: db,
Appwrite: appwrite,
}
}
type Message struct {
ID string `json:"id"`
ConversationID string `json:"conversationId"`
SenderID string `json:"senderId"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
IsMine bool `json:"isMine"` // Populated by handler/frontend logic
}
type Conversation struct {
ID string `json:"id"`
CandidateID string `json:"candidateId"`
CompanyID string `json:"companyId"`
JobID *string `json:"jobId"`
LastMessage *string `json:"lastMessage"`
LastMessageAt *time.Time `json:"lastMessageAt"`
ParticipantName string `json:"participantName"`
ParticipantAvatar string `json:"participantAvatar"`
UnreadCount int `json:"unreadCount"`
}
func (s *ChatService) SendMessage(ctx context.Context, senderID, conversationID, content string) (*Message, error) {
// 1. Insert into Postgres
var msgID string
var createdAt time.Time
query := `
INSERT INTO messages (conversation_id, sender_id, content)
VALUES ($1, $2, $3)
RETURNING id, created_at
`
err := s.DB.QueryRowContext(ctx, query, conversationID, senderID, content).Scan(&msgID, &createdAt)
if err != nil {
return nil, fmt.Errorf("failed to insert message: %w", err)
}
// 2. Update Conversation (async-ish if needed, but important for sorting)
updateQuery := `
UPDATE conversations
SET last_message = $1, last_message_at = $2, updated_at = $2
WHERE id = $3
`
if _, err := s.DB.ExecContext(ctx, updateQuery, content, createdAt, conversationID); err != nil {
// Log error but assume message is sent
fmt.Printf("Failed to update conversation: %v\n", err)
}
// 3. Push to Appwrite
go func() {
// Fire and forget for realtime
err := s.Appwrite.PushMessage(context.Background(), msgID, conversationID, senderID, content)
if err != nil {
fmt.Printf("Appwrite push failed: %v\n", err)
}
}()
return &Message{
ID: msgID,
ConversationID: conversationID,
SenderID: senderID,
Content: content,
CreatedAt: createdAt,
}, nil
}
func (s *ChatService) ListMessages(ctx context.Context, conversationID string) ([]Message, error) {
query := `
SELECT id, conversation_id, sender_id, content, created_at
FROM messages
WHERE conversation_id = $1
ORDER BY created_at ASC
`
rows, err := s.DB.QueryContext(ctx, query, conversationID)
if err != nil {
return nil, err
}
defer rows.Close()
var msgs []Message
for rows.Next() {
var m Message
if err := rows.Scan(&m.ID, &m.ConversationID, &m.SenderID, &m.Content, &m.CreatedAt); err != nil {
return nil, err
}
msgs = append(msgs, m)
}
return msgs, nil
}
// ListConversations lists conversations for a user (candidate or company admin)
func (s *ChatService) ListConversations(ctx context.Context, userID, tenantID string, isCandidate bool) ([]Conversation, error) {
var query string
var args []interface{}
if isCandidate {
// User is candidate -> Fetch company info
query = `
SELECT c.id, c.candidate_id, c.company_id, c.job_id, c.last_message, c.last_message_at,
comp.name as participant_name
FROM conversations c
JOIN companies comp ON c.company_id = comp.id
WHERE c.candidate_id = $1
ORDER BY c.last_message_at DESC NULLS LAST
`
args = append(args, userID)
} else if tenantID != "" {
// User is company admin -> Fetch candidate info
query = `
SELECT c.id, c.candidate_id, c.company_id, c.job_id, c.last_message, c.last_message_at,
COALESCE(u.name, u.full_name, u.identifier) as participant_name
FROM conversations c
JOIN users u ON c.candidate_id = u.id
WHERE c.company_id = $1
ORDER BY c.last_message_at DESC NULLS LAST
`
args = append(args, tenantID)
} else {
return nil, fmt.Errorf("invalid context for listing conversations")
}
rows, err := s.DB.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var convs []Conversation
for rows.Next() {
var c Conversation
if err := rows.Scan(&c.ID, &c.CandidateID, &c.CompanyID, &c.JobID, &c.LastMessage, &c.LastMessageAt, &c.ParticipantName); err != nil {
return nil, err
}
convs = append(convs, c)
}
return convs, nil
}