Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/browser_manager/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func Init(ctx context.Context, tabPool int) error {
return fmt.Errorf("ROD_BROWSER_BIN environment variable not set")
}

userDataDir := fmt.Sprintf("/tmp/chrome-user-data-%d", os.Getpid())

launcher := launcher.New().Bin(browserPath).
Headless(true).
Set("--disable-gpu").
Expand All @@ -36,7 +38,7 @@ func Init(ctx context.Context, tabPool int) error {
Set("--disable-sync").
Set("--metrics-recording-only").
Set("--mute-audio").
Set("--user-data-dir", "/tmp/chrome-user-data").
Set("--user-data-dir", userDataDir).
Set("--disable-web-security").
Set("--no-startup-window").
Set("--disable-renderer-backgrounding"). // Prevent background throttling
Expand All @@ -61,7 +63,7 @@ func Init(ctx context.Context, tabPool int) error {
Set("--disable-cache").
Set("--disable-prompt-on-repost").
Set("--disable-domain-reliability").
Set("--disable-features", "NetworkService,OutOfBlinkCors,InterestGroupStorage,UserAgentClientHint").
Set("--disable-features", "OutOfBlinkCors,InterestGroupStorage,UserAgentClientHint").
Set("--disable-extensions").
Set("--disable-component-extensions-with-background-pages").
Set("--blink-settings", "autoplayPolicy=document-user-activation-required").
Expand Down
7 changes: 5 additions & 2 deletions service/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ RUN mkdir -p /app/espresso/configs \

# Configure Go to bypass proxy for private repositories
# Set ENABLE_UI to false to disable the UI
ENV GOSUMDB=off \
ENV GOMODCACHE=/home/chrome/.cache/go-mod \
GOCACHE=/home/chrome/.cache/go-build \
GOSUMDB=off \
GO111MODULE=on \
ROD_BROWSER_BIN=/usr/bin/chromium \
ENABLE_UI=true


COPY lib/* ./lib

# Copy go.mod and go.sum first to leverage Docker cache
COPY service/go.mod service/go.sum ./service/
Expand Down
8 changes: 7 additions & 1 deletion service/configs/espressoconfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ template_storage:
file_storage:
storage_type: "disk"

mcp:
enabled: true
pdf_output_dir: "./output"
pdf_output_url_prefix: "http://localhost:8081/output"
pdf_output_path: "/output/"

browser:
tab_pool: 50

Expand Down Expand Up @@ -42,4 +48,4 @@ digital_certificates:
key_password: "password2"

mysql:
dsn: "pdf_user:pdf_password@tcp(mysql:3306)/pdf_templates?parseTime=true"
dsn: "pdf_user:pdf_password@tcp(localhost:3308)/pdf_templates?parseTime=true"
113 changes: 113 additions & 0 deletions service/controller/http/pdf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package controller

import (
"encoding/json"
"net/http"
"path/filepath"
"strings"

"github.com/Zomato/espresso/service/dto"
"github.com/Zomato/espresso/service/pkg/response"
"github.com/Zomato/espresso/service/service"
svcUtils "github.com/Zomato/espresso/service/utils"
)

type PDFController struct {
service *service.PDFService
}

func NewPDFController(service *service.PDFService) *PDFController {
return &PDFController{service: service}
}

func (s *PDFController) GeneratePDF(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := &dto.GeneratePDFRequest{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
svcUtils.Logger.Error(ctx, "error decoding request body", err, nil)
response.RespondWithError(w, "Failed to parse JSON request", http.StatusBadRequest)
return
}

resp, err := s.service.GeneratePDF(ctx, req)
if err != nil {
svcUtils.Logger.Error(ctx, "error in generating pdf", err, nil)
response.RespondWithError(w, "Failed to generate PDF: "+err.Error(), http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}

func (s *PDFController) GeneratePDFStream(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req dto.PDFRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
svcUtils.Logger.Error(ctx, "error decoding request body", err, nil)
response.RespondWithError(w, "Failed to parse JSON request", http.StatusBadRequest)
return
}

if err := req.Validate(); err != nil {
response.RespondWithError(w, err.Error(), http.StatusBadRequest)
return
}

resp, err := s.service.GeneratePDFStream(ctx, &req)
if err != nil {
svcUtils.Logger.Error(ctx, "error in generating pdf stream", err, nil)
response.RespondWithError(w, "Failed to generate PDF stream: "+err.Error(), http.StatusInternalServerError)
return
}

fileName := "generated.pdf"
if req.Filename != "" {
fileName = req.Filename
if !strings.HasSuffix(strings.ToLower(fileName), ".pdf") {
fileName += ".pdf"
}
}
fileName = filepath.Base(fileName)

if len(resp.OutputFileBytes) == 0 {
response.RespondWithError(w, "No PDF data available", http.StatusInternalServerError)
return
}

if err = response.RespondWithFile(w, fileName, "application/pdf", resp.OutputFileBytes); err != nil {
svcUtils.Logger.Error(ctx, "error writing pdf stream", err, nil)
response.RespondWithError(w, "Failed to write PDF stream: "+err.Error(), http.StatusInternalServerError)
return
}
}

func (s *PDFController) SignPDF(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := &dto.SignPDFRequest{}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
svcUtils.Logger.Error(ctx, "error decoding request body", err, nil)
response.RespondWithError(w, "Error decoding request body: "+err.Error(), http.StatusBadRequest)
return
}

resp, err := s.service.SignPDF(ctx, req)
if err != nil {
svcUtils.Logger.Error(ctx, "error in signing pdf", err, nil)
response.RespondWithError(w, "Failed to sign PDF: "+err.Error(), http.StatusInternalServerError)
return
}

responseData := map[string]interface{}{
"status": map[string]string{
"status": "success",
"message": "PDF signed successfully",
},
"output_file_path": resp.OutputFilePath,
"output_file_bytes": resp.OutputFileBytes,
}

w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(responseData)
}
83 changes: 83 additions & 0 deletions service/controller/http/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package controller

import (
"encoding/json"
"net/http"

"github.com/Zomato/espresso/service/dto"
"github.com/Zomato/espresso/service/pkg/response"
"github.com/Zomato/espresso/service/service"
svcUtils "github.com/Zomato/espresso/service/utils"
)

type TemplateController struct {
service *service.TemplateService
}

func NewTemplateController(service *service.TemplateService) *TemplateController {
return &TemplateController{service: service}
}

func (s *TemplateController) GetAllTemplates(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

resp, err := s.service.GetAllTemplates(ctx)
if err != nil {
svcUtils.Logger.Error(ctx, "error listing templates", err, nil)
response.RespondWithError(w, "Failed to list templates: "+err.Error(), http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}

func (s *TemplateController) GetTemplateById(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

templateID := r.URL.Query().Get("template_id")
if templateID == "" {
svcUtils.Logger.Error(ctx, "template id is required", nil, nil)
response.RespondWithError(w, "template id is required", http.StatusBadRequest)
return
}

resp, err := s.service.GetTemplateById(ctx, &dto.GetTemplateByIdRequest{TemplateId: templateID})
if err != nil {
svcUtils.Logger.Error(ctx, "error getting template content", err, nil)
response.RespondWithError(w, "Failed to get template content: "+err.Error(), http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}

func (s *TemplateController) CreateTemplate(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := &dto.CreateTemplateRequest{}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
svcUtils.Logger.Error(ctx, "error decoding request body", err, nil)
response.RespondWithError(w, "Error decoding request body: "+err.Error(), http.StatusBadRequest)
return
}

resp, err := s.service.CreateTemplate(ctx, req)
if err != nil {
svcUtils.Logger.Error(ctx, "error creating template", err, nil)
response.RespondWithError(w, "Failed to create template: "+err.Error(), http.StatusInternalServerError)
return
}

responseData := map[string]interface{}{
"status": map[string]string{
"status": "success",
"message": "Template created successfully",
},
"template_id": resp.TemplateId,
}

w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(responseData)
}
107 changes: 107 additions & 0 deletions service/controller/mcp/pdf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package tools

import (
"context"

"github.com/Zomato/espresso/service/dto"
pdfservice "github.com/Zomato/espresso/service/service"
svcUtils "github.com/Zomato/espresso/service/utils"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

type PDFTools struct {
service *pdfservice.PDFService
}

func NewPDFTools(service *pdfservice.PDFService) *PDFTools {
return &PDFTools{service: service}
}

func (s *PDFTools) GeneratePDF(ctx context.Context, _ *mcp.CallToolRequest, req dto.GeneratePDFMCPRequest) (*mcp.CallToolResult, dto.GeneratePDFMCPResponse, error) {
generatePDFReq, err := req.GeneratePDFMCPRequestToGeneratePDFRequest()
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "Failed to parse generate PDF request: " + err.Error()}},
IsError: true,
}, dto.GeneratePDFMCPResponse{}, nil
}

svcResp, err := s.service.GeneratePDF(ctx, generatePDFReq)
if err != nil {
svcUtils.Logger.Error(ctx, "error in generating pdf", err, nil)
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "Failed to generate PDF: " + err.Error()}},
IsError: true,
}, dto.GeneratePDFMCPResponse{}, nil
}

resp := svcResp.GeneratePDFResponseToGeneratePDFMCPResponse()
return nil, *resp, nil
}

// func (s *PDFTools) GeneratePDFStream(ctx context.Context, _ *mcp.CallToolRequest, req dto.PDFMCPRequest) (*mcp.CallToolResult, any, error) {
// pdfReq, err := req.PDFMCPRequestToPDFRequest()
// if err != nil {
// return &mcp.CallToolResult{
// Content: []mcp.Content{&mcp.TextContent{Text: "Failed to parse generate PDF stream request: " + err.Error()}},
// IsError: true,
// }, nil, nil
// }

// if err := pdfReq.Validate(); err != nil {
// return &mcp.CallToolResult{
// Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}},
// IsError: true,
// }, nil, nil
// }

// resp, err := s.service.GeneratePDFStream(ctx, pdfReq)
// if err != nil {
// svcUtils.Logger.Error(ctx, "error in generating pdf stream", err, nil)
// return &mcp.CallToolResult{
// Content: []mcp.Content{&mcp.TextContent{Text: "Failed to generate PDF stream: " + err.Error()}},
// IsError: true,
// }, nil, nil
// }

// fileName := "generated.pdf"
// if pdfReq.Filename != "" {
// fileName = pdfReq.Filename
// if !strings.HasSuffix(strings.ToLower(fileName), ".pdf") {
// fileName += ".pdf"
// }
// }
// fileName = filepath.Base(fileName)

// if len(resp.OutputFileBytes) == 0 {
// return &mcp.CallToolResult{
// Content: []mcp.Content{&mcp.TextContent{Text: "No PDF data available"}},
// IsError: true,
// }, nil, nil
// }

// return &mcp.CallToolResult{
// Content: []mcp.Content{
// &mcp.EmbeddedResource{
// Resource: &mcp.ResourceContents{
// URI: "file://" + fileName,
// MIMEType: "application/pdf",
// Blob: resp.OutputFileBytes,
// },
// },
// },
// }, nil, nil
// }

func (s *PDFTools) SignPDF(ctx context.Context, _ *mcp.CallToolRequest, req dto.SignPDFRequest) (*mcp.CallToolResult, dto.SignPDFResponse, error) {
resp, err := s.service.SignPDF(ctx, &req)
if err != nil {
svcUtils.Logger.Error(ctx, "error in signing pdf", err, nil)
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "Failed to sign PDF: " + err.Error()}},
IsError: true,
}, dto.SignPDFResponse{}, nil
}

return nil, *resp, nil
}
Loading