// Package officeconvertclient provides typed Connect RPC helpers for conversions. package officeconvertclient import ( "context" "errors" "net/http" "time" "connectrpc.com/connect" officeconvertapiv1 "gitea.auvem.com/end-internal/officeconvert/gen/go/officeconvertapi/v1" "gitea.auvem.com/end-internal/officeconvert/gen/go/officeconvertapi/v1/officeconvertapiv1connect" "github.com/segmentio/ksuid" ) const defaultPollInterval = 2 * time.Second // RasterTierOptions defines per-tier raster settings for conversion output. type RasterTierOptions struct { Resolution officeconvertapiv1.ConversionResolution JPEGQuality int32 } // HtmlFormattingPolicy configures which formatting features are ignored in HTML notes mode. type HtmlFormattingPolicy struct { IgnoreBold bool IgnoreItalic bool IgnoreUnderline bool IgnoreStrikethrough bool IgnoreFontSize bool IgnoreColor bool } // NotesOptions controls speaker-notes extraction/output. type NotesOptions struct { Format officeconvertapiv1.NotesFormat HTMLUseParagraphTags *bool HTMLPolicy *HtmlFormattingPolicy } // CreateConversionOptions configures optional full and thumbnail raster tiers. type CreateConversionOptions struct { Full *RasterTierOptions Thumbnail *RasterTierOptions Notes *NotesOptions } // Client wraps the generated Connect client with orchestration-focused helpers. type Client struct { rpc officeconvertapiv1connect.ConversionServiceClient httpClient *http.Client baseURL string pollInterval time.Duration } // NewClient creates a new typed Officeconvert client. func NewClient(baseURL string, httpClient *http.Client, options ...connect.ClientOption) *Client { if httpClient == nil { httpClient = http.DefaultClient } return &Client{ rpc: officeconvertapiv1connect.NewConversionServiceClient(httpClient, baseURL, options...), httpClient: httpClient, baseURL: baseURL, pollInterval: defaultPollInterval, } } // SetPollInterval configures the polling cadence used by WaitForCompletion. func (c *Client) SetPollInterval(interval time.Duration) { if interval > 0 { c.pollInterval = interval } } // CreateConversion starts a conversion session and returns upload metadata. func (c *Client) CreateConversion( ctx context.Context, sourceFilename string, options *CreateConversionOptions, ) (*CreateConversionResponse, error) { message := &officeconvertapiv1.CreateConversionRequest{ SourceFilename: sourceFilename, } if options != nil { message.Full = toProtoSlideRasterOptions(options.Full) message.Thumbnail = toProtoSlideRasterOptions(options.Thumbnail) message.Notes = toProtoNotesOptions(options.Notes) } req := connect.NewRequest(message) res, err := c.rpc.CreateConversion(ctx, req) if err != nil { return nil, err } return augmentCreateConversionResponse(res.Msg) } func toProtoSlideRasterOptions( options *RasterTierOptions, ) *officeconvertapiv1.SlideRasterOptions { if options == nil { return nil } proto := &officeconvertapiv1.SlideRasterOptions{ Resolution: options.Resolution, } if options.JPEGQuality != 0 { proto.Format = &officeconvertapiv1.SlideRasterOptions_Jpeg{ Jpeg: &officeconvertapiv1.JpegOutputOptions{ Quality: options.JPEGQuality, }, } } return proto } func toProtoNotesOptions(options *NotesOptions) *officeconvertapiv1.NotesOptions { if options == nil { return nil } proto := &officeconvertapiv1.NotesOptions{ Format: options.Format, } if options.HTMLUseParagraphTags != nil { proto.HtmlUseParagraphTags = options.HTMLUseParagraphTags } if options.HTMLPolicy != nil { proto.HtmlPolicy = &officeconvertapiv1.HtmlFormattingPolicy{ IgnoreBold: options.HTMLPolicy.IgnoreBold, IgnoreItalic: options.HTMLPolicy.IgnoreItalic, IgnoreUnderline: options.HTMLPolicy.IgnoreUnderline, IgnoreStrikethrough: options.HTMLPolicy.IgnoreStrikethrough, IgnoreFontSize: options.HTMLPolicy.IgnoreFontSize, IgnoreColor: options.HTMLPolicy.IgnoreColor, } } return proto } // StartConversion signals that upload is complete and conversion can begin. func (c *Client) StartConversion( ctx context.Context, id ksuid.KSUID, ) (*StartConversionResponse, error) { req := connect.NewRequest(&officeconvertapiv1.StartConversionRequest{ ConversionId: id.String(), }) res, err := c.rpc.StartConversion(ctx, req) if err != nil { return nil, err } return augmentStartConversionResponse(res.Msg) } // GetConversionStatus returns the latest status for a conversion session. func (c *Client) GetConversionStatus( ctx context.Context, id ksuid.KSUID, ) (*GetConversionStatusResponse, error) { req := connect.NewRequest(&officeconvertapiv1.GetConversionStatusRequest{ ConversionId: id.String(), }) res, err := c.rpc.GetConversionStatus(ctx, req) if err != nil { return nil, err } return augmentGetConversionStatusResponse(res.Msg) } // GetSlideDeck retrieves the final converted deck response. func (c *Client) GetSlideDeck( ctx context.Context, id ksuid.KSUID, ) (*officeconvertapiv1.GetSlideDeckResponse, error) { req := connect.NewRequest(&officeconvertapiv1.GetSlideDeckRequest{ ConversionId: id.String(), }) res, err := c.rpc.GetSlideDeck(ctx, req) if err != nil { return nil, err } return res.Msg, nil } // DeleteConversion triggers immediate resource cleanup for a session. func (c *Client) DeleteConversion( ctx context.Context, id ksuid.KSUID, ) (*DeleteConversionResponse, error) { req := connect.NewRequest(&officeconvertapiv1.DeleteConversionRequest{ ConversionId: id.String(), }) res, err := c.rpc.DeleteConversion(ctx, req) if err != nil { return nil, err } return augmentDeleteConversionResponse(res.Msg) } // WaitForCompletion polls status until terminal completion or context cancellation. func (c *Client) WaitForCompletion( ctx context.Context, id ksuid.KSUID, ) (*GetConversionStatusResponse, error) { ticker := time.NewTicker(c.pollInterval) defer ticker.Stop() for { status, err := c.GetConversionStatus(ctx, id) if err != nil { return nil, err } switch status.Status { case officeconvertapiv1.ConversionStatus_CONVERSION_STATUS_SUCCEEDED: return status, nil case officeconvertapiv1.ConversionStatus_CONVERSION_STATUS_FAILED: return nil, errors.New(status.ErrorMessage) } select { case <-ctx.Done(): return nil, ctx.Err() case <-ticker.C: } } }