package grpcserver import ( "context" "errors" "log/slog" "time" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" mcdslgrpc "git.wntrmute.dev/mc/mcdsl/grpcserver" pb "git.wntrmute.dev/mc/mcq/gen/mcq/v1" "git.wntrmute.dev/mc/mcq/internal/db" ) type documentService struct { pb.UnimplementedDocumentServiceServer db *db.DB logger *slog.Logger } func (s *documentService) ListDocuments(_ context.Context, _ *pb.ListDocumentsRequest) (*pb.ListDocumentsResponse, error) { docs, err := s.db.ListDocuments() if err != nil { return nil, status.Error(codes.Internal, "failed to list documents") } resp := &pb.ListDocumentsResponse{} for _, d := range docs { resp.Documents = append(resp.Documents, s.docToProto(d)) } return resp, nil } func (s *documentService) GetDocument(_ context.Context, req *pb.GetDocumentRequest) (*pb.Document, error) { if req.Slug == "" { return nil, status.Error(codes.InvalidArgument, "slug is required") } doc, err := s.db.GetDocument(req.Slug) if errors.Is(err, db.ErrNotFound) { return nil, status.Error(codes.NotFound, "document not found") } if err != nil { return nil, status.Error(codes.Internal, "failed to get document") } return s.docToProto(*doc), nil } func (s *documentService) PutDocument(ctx context.Context, req *pb.PutDocumentRequest) (*pb.Document, error) { if req.Slug == "" { return nil, status.Error(codes.InvalidArgument, "slug is required") } if req.Title == "" { return nil, status.Error(codes.InvalidArgument, "title is required") } if req.Body == "" { return nil, status.Error(codes.InvalidArgument, "body is required") } pushedBy := "unknown" if info := mcdslgrpc.TokenInfoFromContext(ctx); info != nil { pushedBy = info.Username } doc, err := s.db.PutDocument(req.Slug, req.Title, req.Body, pushedBy) if err != nil { return nil, status.Error(codes.Internal, "failed to save document") } return s.docToProto(*doc), nil } func (s *documentService) DeleteDocument(_ context.Context, req *pb.DeleteDocumentRequest) (*pb.DeleteDocumentResponse, error) { if req.Slug == "" { return nil, status.Error(codes.InvalidArgument, "slug is required") } err := s.db.DeleteDocument(req.Slug) if errors.Is(err, db.ErrNotFound) { return nil, status.Error(codes.NotFound, "document not found") } if err != nil { return nil, status.Error(codes.Internal, "failed to delete document") } return &pb.DeleteDocumentResponse{}, nil } func (s *documentService) MarkRead(_ context.Context, req *pb.MarkReadRequest) (*pb.Document, error) { if req.Slug == "" { return nil, status.Error(codes.InvalidArgument, "slug is required") } doc, err := s.db.MarkRead(req.Slug) if errors.Is(err, db.ErrNotFound) { return nil, status.Error(codes.NotFound, "document not found") } if err != nil { return nil, status.Error(codes.Internal, "failed to mark read") } return s.docToProto(*doc), nil } func (s *documentService) MarkUnread(_ context.Context, req *pb.MarkUnreadRequest) (*pb.Document, error) { if req.Slug == "" { return nil, status.Error(codes.InvalidArgument, "slug is required") } doc, err := s.db.MarkUnread(req.Slug) if errors.Is(err, db.ErrNotFound) { return nil, status.Error(codes.NotFound, "document not found") } if err != nil { return nil, status.Error(codes.Internal, "failed to mark unread") } return s.docToProto(*doc), nil } func (s *documentService) docToProto(d db.Document) *pb.Document { return &pb.Document{ Id: d.ID, Slug: d.Slug, Title: d.Title, Body: d.Body, PushedBy: d.PushedBy, PushedAt: s.parseTimestamp(d.PushedAt), Read: d.Read, } } func (s *documentService) parseTimestamp(v string) *timestamppb.Timestamp { t, err := time.Parse(time.RFC3339, v) if err != nil { s.logger.Warn("failed to parse timestamp", "value", v, "error", err) return nil } return timestamppb.New(t) }