Files
mcq/internal/grpcserver/documents.go
Kyle Isom bc1627915e Initial implementation of mcq — document reading queue
Single-binary service: push raw markdown via REST/gRPC API, read rendered
HTML through mobile-friendly web UI. MCIAS auth on all endpoints, SQLite
storage, goldmark rendering with GFM and syntax highlighting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:53:26 -07:00

141 lines
3.9 KiB
Go

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