package service import ( "context" "fmt" "log/slog" "time" "github.com/owncord/server/db" "github.com/owncord/server/telemetry" ) // DMService handles direct message channel operations. type DMService struct { st Store } // NewDMService creates a DMService. func NewDMService(st Store) *DMService { return &DMService{st: st} } // CreateDMResult holds the result of creating or fetching a DM channel. type CreateDMResult struct { Channel *db.Channel Created bool Recipient *db.User } // CreateDM creates or retrieves a DM channel between two users. // Validates that neither user has blocked the other. func (s *DMService) CreateDM(ctx context.Context, userID, recipientID int64) (*CreateDMResult, error) { ctx, span := telemetry.GlobalTracer("service/dm").Start(ctx, "DMService.CreateDM", telemetry.Int64("user_id", userID), telemetry.Int64("recipient_id", recipientID), ) start := time.Now() defer func() { telemetry.TimeSince(ctx, telemetry.NewAppMetrics().ServiceCallDurationSec, start, telemetry.String("method", "CreateDM")) span.End() }() if recipientID <= 0 { return nil, fmt.Errorf("%w: recipient_id must be positive", ErrBadRequest) } if userID == recipientID { return nil, fmt.Errorf("%w: cannot create DM with yourself", ErrBadRequest) } recipient, err := s.st.GetUserByID(ctx, recipientID) if err != nil || recipient == nil { return nil, fmt.Errorf("%w: recipient not found", ErrNotFound) } blocked, err := s.st.IsEitherBlocked(ctx, userID, recipientID) if err != nil { return nil, fmt.Errorf("%w: failed to check block status: %v", ErrInternal, err) } if blocked { return nil, fmt.Errorf("%w: cannot create DM — user is blocked", ErrForbidden) } ch, created, err := s.st.GetOrCreateDMChannel(ctx, userID, recipientID) if err != nil { slog.Error("DMService.CreateDM", "err", err) return nil, fmt.Errorf("%w: failed to create DM channel", ErrInternal) } return &CreateDMResult{ Channel: ch, Created: created, Recipient: recipient, }, nil } // ListDMs returns all open DM channels for a user. func (s *DMService) ListDMs(ctx context.Context, userID int64) ([]db.DMChannelInfo, error) { dms, err := s.st.GetUserDMChannels(ctx, userID) if err != nil { return nil, fmt.Errorf("%w: failed to list DMs: %v", ErrInternal, err) } return dms, nil } // CloseDM closes a DM channel for a user. func (s *DMService) CloseDM(ctx context.Context, userID, channelID int64) error { if channelID <= 0 { return fmt.Errorf("%w: channel_id must be positive", ErrBadRequest) } ok, err := s.st.IsDMParticipant(ctx, userID, channelID) if err != nil || !ok { return fmt.Errorf("%w: not a participant in this DM", ErrNotFound) } if err := s.st.CloseDM(ctx, userID, channelID); err != nil { return fmt.Errorf("%w: failed to close DM: %v", ErrInternal, err) } slog.Debug("DM closed", "user_id", userID, "channel_id", channelID) return nil }