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>
This commit is contained in:
140
internal/grpcserver/documents.go
Normal file
140
internal/grpcserver/documents.go
Normal file
@@ -0,0 +1,140 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user