package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "image/color" "log" "math" "net/http" "net/url" "os" "strings" "time" "github.com/fogleman/gg" "golang.org/x/image/font" "golang.org/x/image/font/gofont/gobold" "golang.org/x/image/font/gofont/goregular" "golang.org/x/image/font/opentype" "golang.org/x/text/currency" "golang.org/x/text/language" "golang.org/x/text/message" ) const ( imageWidth = 1200 imageHeight = 630 ) type shareData struct { Name string `json:"n"` RealAmount json.Number `json:"r"` GoalAmount json.Number `json:"g"` Currency string `json:"c"` TargetDate string `json:"td"` EstimateCompletion string `json:"ec"` PrivateMode bool `json:"p"` } type shareView struct { Title string Description string OGImageURL string ShareURL string GoalName string CurrentAmount string TargetAmount string PercentText string ProgressPercent float64 TargetDateLabel string EstimateDateLabel string PrivateMode bool FormattedGoalAmount string } func main() { port := os.Getenv("PORT") if port == "" { port = "8090" } http.HandleFunc("/share", shareHandler) http.HandleFunc("/og", ogHandler) log.Printf("og-service listening on :%s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } func shareHandler(w http.ResponseWriter, r *http.Request) { dataParam := r.URL.Query().Get("data") if dataParam == "" { http.Error(w, "missing data parameter", http.StatusBadRequest) return } payload, err := decodeData(dataParam) if err != nil { http.Error(w, "invalid data parameter", http.StatusBadRequest) return } view := buildShareView(payload, r, dataParam) html := renderShareHTML(view) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _, _ = w.Write([]byte(html)) } func ogHandler(w http.ResponseWriter, r *http.Request) { dataParam := r.URL.Query().Get("data") if dataParam == "" { http.Error(w, "missing data parameter", http.StatusBadRequest) return } payload, err := decodeData(dataParam) if err != nil { http.Error(w, "invalid data parameter", http.StatusBadRequest) return } pngBytes, err := renderOGImage(payload) if err != nil { http.Error(w, "failed to render image", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "image/png") w.Header().Set("Cache-Control", "public, max-age=3600") _, _ = w.Write(pngBytes) } func decodeData(param string) (shareData, error) { var data shareData decoded, err := base64.RawURLEncoding.DecodeString(param) if err != nil { decoded, err = base64.URLEncoding.DecodeString(param) if err != nil { return data, err } } dec := json.NewDecoder(bytes.NewReader(decoded)) dec.UseNumber() if err := dec.Decode(&data); err != nil { return data, err } data.Name = strings.TrimSpace(data.Name) data.Currency = strings.ToUpper(strings.TrimSpace(data.Currency)) if data.Currency == "" { data.Currency = "USD" } return data, nil } func buildShareView(data shareData, r *http.Request, rawParam string) shareView { goalAmount, _ := numberToFloat(data.GoalAmount) realAmount, _ := numberToFloat(data.RealAmount) percent := 0.0 if goalAmount > 0 { percent = (realAmount / goalAmount) * 100 } percentText := "Progress unavailable" if goalAmount > 0 { percentText = fmt.Sprintf("%.0f%% complete", percent) } currencyTag := localeForCurrency(data.Currency) formattedGoal := formatCurrency(goalAmount, data.Currency, currencyTag) formattedReal := formatCurrency(realAmount, data.Currency, currencyTag) currentAmount := formattedReal if data.PrivateMode { currentAmount = "Hidden" } goalName := data.Name if goalName == "" { goalName = "Investment Goal" } description := fmt.Sprintf("%s toward %s", percentText, formattedGoal) if data.PrivateMode { description = fmt.Sprintf("Goal progress: %s", percentText) } baseURL := buildBaseURL(r) shareURL := fmt.Sprintf("%s/share?data=%s", baseURL, url.QueryEscape(rawParam)) ogImageURL := fmt.Sprintf("%s/og?data=%s", baseURL, url.QueryEscape(rawParam)) return shareView{ Title: fmt.Sprintf("Portfolio Journal — %s", goalName), Description: description, OGImageURL: ogImageURL, ShareURL: shareURL, GoalName: goalName, CurrentAmount: currentAmount, TargetAmount: formattedGoal, PercentText: percentText, ProgressPercent: percent, TargetDateLabel: formatDateLabel("Target", data.TargetDate), EstimateDateLabel: formatDateLabel("Est. completion", data.EstimateCompletion), PrivateMode: data.PrivateMode, FormattedGoalAmount: formattedGoal, } } func renderShareHTML(view shareView) string { htmlTemplate := ` ` + escapeHTML(view.Title) + `
Portfolio Journal

` + escapeHTML(view.GoalName) + `

Current amount ` + escapeHTML(view.CurrentAmount) + `
Goal amount ` + escapeHTML(view.TargetAmount) + `
` + escapeHTML(view.PercentText) + `
` + escapeHTML(joinLabels(view.TargetDateLabel, view.EstimateDateLabel)) + `
Download on the App Store
` return htmlTemplate } func renderOGImage(data shareData) ([]byte, error) { goalAmount, _ := numberToFloat(data.GoalAmount) realAmount, _ := numberToFloat(data.RealAmount) percent := 0.0 if goalAmount > 0 { percent = (realAmount / goalAmount) * 100 } currencyTag := localeForCurrency(data.Currency) formattedGoal := formatCurrency(goalAmount, data.Currency, currencyTag) formattedReal := formatCurrency(realAmount, data.Currency, currencyTag) if data.PrivateMode { formattedReal = "Hidden" } goalName := data.Name if goalName == "" { goalName = "Investment Goal" } dc := gg.NewContext(imageWidth, imageHeight) grad := gg.NewLinearGradient(0, 0, imageWidth, imageHeight) grad.AddColorStop(0, color.RGBA{0x66, 0x7e, 0xea, 0xff}) grad.AddColorStop(1, color.RGBA{0x76, 0x4b, 0xa2, 0xff}) dc.SetFillStyle(grad) dc.DrawRectangle(0, 0, imageWidth, imageHeight) dc.Fill() marginX := 80.0 marginY := 80.0 // App icon placeholder dc.SetRGBA(1, 1, 1, 0.2) dc.DrawRoundedRectangle(marginX, marginY, 96, 96, 22) dc.Fill() iconFace, err := loadFontFace(38, true) if err != nil { return nil, err } dc.SetFontFace(iconFace) dc.SetRGBA(1, 1, 1, 0.9) dc.DrawStringAnchored("PJ", marginX+48, marginY+52, 0.5, 0.5) labelFace, err := loadFontFace(20, false) if err != nil { return nil, err } dc.SetFontFace(labelFace) dc.SetRGBA(1, 1, 1, 0.8) dc.DrawString("Portfolio Journal", marginX+120, marginY+34) titleFace, err := loadFontFace(54, true) if err != nil { return nil, err } dc.SetFontFace(titleFace) dc.SetRGBA(1, 1, 1, 1) goalName = fitText(dc, goalName, imageWidth-160) dc.DrawStringWrapped(goalName, marginX, marginY+130, 0, 0, imageWidth-160, 1.3, gg.AlignLeft) valueFace, err := loadFontFace(36, true) if err != nil { return nil, err } labelSmallFace, err := loadFontFace(18, false) if err != nil { return nil, err } dc.SetFontFace(labelSmallFace) dc.SetRGBA(1, 1, 1, 0.75) dc.DrawString("Current", marginX, marginY+260) dc.DrawString("Goal", marginX+420, marginY+260) dc.SetFontFace(valueFace) dc.SetRGBA(1, 1, 1, 1) dc.DrawString(formattedReal, marginX, marginY+305) dc.DrawString(formattedGoal, marginX+420, marginY+305) percentText := "Progress unavailable" if goalAmount > 0 { percentText = fmt.Sprintf("%.0f%% complete", percent) } if data.PrivateMode { percentText = strings.ReplaceAll(percentText, "complete", "of goal") } dc.SetFontFace(labelSmallFace) dc.SetRGBA(1, 1, 1, 0.85) dc.DrawString(percentText, marginX, marginY+360) barWidth := 720.0 barHeight := 18.0 barX := marginX barY := marginY + 380 progress := math.Max(0, math.Min(100, percent)) / 100 dc.SetRGBA(1, 1, 1, 0.25) dc.DrawRoundedRectangle(barX, barY, barWidth, barHeight, 9) dc.Fill() dc.SetRGBA(1, 1, 1, 0.9) dc.DrawRoundedRectangle(barX, barY, barWidth*progress, barHeight, 9) dc.Fill() meta := joinLabels(formatDateLabel("Target", data.TargetDate), formatDateLabel("Est. completion", data.EstimateCompletion)) if meta != "" { dc.SetFontFace(labelSmallFace) dc.SetRGBA(1, 1, 1, 0.7) dc.DrawString(meta, marginX, marginY+430) } ctaFace, err := loadFontFace(20, true) if err != nil { return nil, err } dc.SetFontFace(ctaFace) dc.SetRGBA(1, 1, 1, 0.9) dc.DrawString("Available on the App Store", marginX, imageHeight-70) buf := bytes.NewBuffer(nil) if err := dc.EncodePNG(buf); err != nil { return nil, err } return buf.Bytes(), nil } func numberToFloat(n json.Number) (float64, bool) { if n.String() == "" { return 0, false } value, err := n.Float64() if err != nil { return 0, false } return value, true } func formatCurrency(amount float64, code string, tag language.Tag) string { unit, err := currency.ParseISO(code) if err != nil { unit = currency.USD } printer := message.NewPrinter(tag) return printer.Sprintf("%v", currency.Amount(amount, unit)) } func localeForCurrency(code string) language.Tag { switch code { case "EUR": return language.MustParse("es-ES") case "GBP": return language.MustParse("en-GB") case "JPY": return language.MustParse("ja-JP") case "CNY": return language.MustParse("zh-CN") case "HKD": return language.MustParse("zh-HK") case "KRW": return language.MustParse("ko-KR") case "INR": return language.MustParse("en-IN") case "BRL": return language.MustParse("pt-BR") case "MXN": return language.MustParse("es-MX") case "ARS": return language.MustParse("es-AR") case "CLP": return language.MustParse("es-CL") case "COP": return language.MustParse("es-CO") case "CAD": return language.MustParse("en-CA") case "AUD": return language.MustParse("en-AU") case "NZD": return language.MustParse("en-NZ") case "CHF": return language.MustParse("de-CH") case "SEK": return language.MustParse("sv-SE") case "NOK": return language.MustParse("nb-NO") case "DKK": return language.MustParse("da-DK") case "PLN": return language.MustParse("pl-PL") case "CZK": return language.MustParse("cs-CZ") case "HUF": return language.MustParse("hu-HU") case "TRY": return language.MustParse("tr-TR") case "ZAR": return language.MustParse("en-ZA") case "AED": return language.MustParse("ar-AE") default: return language.MustParse("en-US") } } func formatDateLabel(prefix, dateString string) string { if dateString == "" { return "" } parsed, err := time.Parse("2006-01-02", dateString) if err != nil { return "" } return fmt.Sprintf("%s: %s", prefix, parsed.Format("Jan 2, 2006")) } func joinLabels(primary, secondary string) string { if primary == "" && secondary == "" { return "" } if primary == "" { return secondary } if secondary == "" { return primary } return primary + " • " + secondary } func buildBaseURL(r *http.Request) string { proto := r.Header.Get("X-Forwarded-Proto") if proto == "" { proto = "https" } host := r.Header.Get("X-Forwarded-Host") if host == "" { host = r.Host } return fmt.Sprintf("%s://%s", proto, host) } func escapeHTML(input string) string { replacer := strings.NewReplacer( "&", "&", "<", "<", ">", ">", "\"", """, "'", "'", ) return replacer.Replace(input) } func loadFontFace(size float64, bold bool) (font.Face, error) { var fontBytes []byte if bold { fontBytes = gobold.TTF } else { fontBytes = goregular.TTF } parsed, err := opentype.Parse(fontBytes) if err != nil { return nil, err } face, err := opentype.NewFace(parsed, &opentype.FaceOptions{ Size: size, DPI: 72, Hinting: font.HintingFull, }) if err != nil { return nil, err } return face, nil } func fitText(dc *gg.Context, text string, maxWidth float64) string { trimmed := strings.TrimSpace(text) if trimmed == "" { return "" } if width, _ := dc.MeasureString(trimmed); width <= maxWidth { return trimmed } runes := []rune(trimmed) for len(runes) > 0 { candidate := string(runes) + "…" if width, _ := dc.MeasureString(candidate); width <= maxWidth { return candidate } runes = runes[:len(runes)-1] } return trimmed }