diff --git a/cmd/handlers.go b/cmd/handlers.go index 002b2704..c117d080 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -37,7 +37,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { // Settings. g.GET("/api/v1/settings/general", auth(handleGetGeneralSettings)) - g.PUT("/api/v1/settings/general", perm(handleUpdateGeneralSettings, "general_settings:manage")) + g.PUT("/api/v1/settings/general", perm(clearsHelpCenterCache(handleUpdateGeneralSettings), "general_settings:manage")) g.GET("/api/v1/settings/notifications/email", perm(handleGetEmailNotificationSettings, "notification_settings:manage")) g.PUT("/api/v1/settings/notifications/email", perm(handleUpdateEmailNotificationSettings, "notification_settings:manage")) @@ -295,24 +295,24 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.GET("/api/v1/help-centers/locales", perm(handleGetHelpCenterLocales, "help_center:manage")) g.GET("/api/v1/help-centers/{id}", perm(handleGetHelpCenter, "help_center:manage")) g.GET("/api/v1/help-centers/{id}/tree", perm(handleGetHelpCenterTree, "help_center:manage")) - g.POST("/api/v1/help-centers", perm(handleCreateHelpCenter, "help_center:manage")) - g.PUT("/api/v1/help-centers/{id}", perm(handleUpdateHelpCenter, "help_center:manage")) + g.POST("/api/v1/help-centers", perm(clearsHelpCenterCache(handleCreateHelpCenter), "help_center:manage")) + g.PUT("/api/v1/help-centers/{id}", perm(clearsHelpCenterCache(handleUpdateHelpCenter), "help_center:manage")) g.POST("/api/v1/help-centers/{id}/preview", perm(handleHelpCenterPreview, "help_center:manage")) - g.PUT("/api/v1/help-centers/{id}/toggle", perm(handleToggleHelpCenterActive, "help_center:manage")) - g.DELETE("/api/v1/help-centers/{id}", perm(handleDeleteHelpCenter, "help_center:manage")) + g.PUT("/api/v1/help-centers/{id}/toggle", perm(clearsHelpCenterCache(handleToggleHelpCenterActive), "help_center:manage")) + g.DELETE("/api/v1/help-centers/{id}", perm(clearsHelpCenterCache(handleDeleteHelpCenter), "help_center:manage")) g.GET("/api/v1/help-centers/{hc_id}/collections", perm(handleGetCollections, "help_center:manage")) - g.POST("/api/v1/help-centers/{hc_id}/collections", perm(handleCreateCollection, "help_center:manage")) - g.PUT("/api/v1/help-centers/{hc_id}/collections/{id}", perm(handleUpdateCollection, "help_center:manage")) - g.DELETE("/api/v1/help-centers/{hc_id}/collections/{id}", perm(handleDeleteCollection, "help_center:manage")) - g.PUT("/api/v1/help-centers/{hc_id}/collection-sort-order", perm(handleUpdateCollectionSortOrders, "help_center:manage")) - g.PUT("/api/v1/collections/{id}/toggle", perm(handleToggleCollection, "help_center:manage")) - g.PUT("/api/v1/collections/{col_id}/article-sort-order", perm(handleUpdateArticleSortOrders, "help_center:manage")) + g.POST("/api/v1/help-centers/{hc_id}/collections", perm(clearsHelpCenterCache(handleCreateCollection), "help_center:manage")) + g.PUT("/api/v1/help-centers/{hc_id}/collections/{id}", perm(clearsHelpCenterCache(handleUpdateCollection), "help_center:manage")) + g.DELETE("/api/v1/help-centers/{hc_id}/collections/{id}", perm(clearsHelpCenterCache(handleDeleteCollection), "help_center:manage")) + g.PUT("/api/v1/help-centers/{hc_id}/collection-sort-order", perm(clearsHelpCenterCache(handleUpdateCollectionSortOrders), "help_center:manage")) + g.PUT("/api/v1/collections/{id}/toggle", perm(clearsHelpCenterCache(handleToggleCollection), "help_center:manage")) + g.PUT("/api/v1/collections/{col_id}/article-sort-order", perm(clearsHelpCenterCache(handleUpdateArticleSortOrders), "help_center:manage")) g.GET("/api/v1/collections/{col_id}/articles/{id}", perm(handleGetArticle, "help_center:manage")) - g.POST("/api/v1/collections/{col_id}/articles", perm(handleCreateArticle, "help_center:manage")) - g.DELETE("/api/v1/collections/{col_id}/articles/{id}", perm(handleDeleteArticle, "help_center:manage")) - g.PUT("/api/v1/articles/{id}", perm(handleUpdateArticle, "help_center:manage")) - g.PUT("/api/v1/articles/{id}/collection", perm(handleMoveArticle, "help_center:manage")) - g.PUT("/api/v1/articles/{id}/status", perm(handleUpdateArticleStatus, "help_center:manage")) + g.POST("/api/v1/collections/{col_id}/articles", perm(clearsHelpCenterCache(handleCreateArticle), "help_center:manage")) + g.DELETE("/api/v1/collections/{col_id}/articles/{id}", perm(clearsHelpCenterCache(handleDeleteArticle), "help_center:manage")) + g.PUT("/api/v1/articles/{id}", perm(clearsHelpCenterCache(handleUpdateArticle), "help_center:manage")) + g.PUT("/api/v1/articles/{id}/collection", perm(clearsHelpCenterCache(handleMoveArticle), "help_center:manage")) + g.PUT("/api/v1/articles/{id}/status", perm(clearsHelpCenterCache(handleUpdateArticleStatus), "help_center:manage")) g.GET("/api/v1/help-centers/{id}/insights", perm(handleGetHelpCenterInsights, "help_center:manage")) // Public help center JSON API. @@ -393,11 +393,11 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { getAndHead("/robots.txt", rateLimit(handleRobotsTxt, "public")) getAndHead("/sitemap.xml", rateLimit(handleSitemapIndex, "public")) getAndHead("/hc/{slug}", rateLimit(handleRedirectHelpCenterHome, "public")) - getAndHead("/hc/{slug}/{locale}", rateLimit(handleShowHelpCenterHome, "public")) + getAndHead("/hc/{slug}/{locale}", rateLimit(cachedHelpCenterPage(handleShowHelpCenterHome), "public")) getAndHead("/hc/{slug}/{locale}/sitemap.xml", rateLimit(handleHelpCenterSitemap, "public")) - getAndHead("/hc/{slug}/{locale}/search", rateLimit(handleHelpCenterSearch, "public")) - getAndHead("/hc/{slug}/{locale}/collections/{collection_slug}", rateLimit(handleShowHelpCenterCollection, "public")) - getAndHead("/hc/{slug}/{locale}/articles/{article_slug}", rateLimit(handleShowHelpCenterArticle, "public")) + getAndHead("/hc/{slug}/{locale}/search", rateLimit(cachedHelpCenterPage(handleHelpCenterSearch), "public")) + getAndHead("/hc/{slug}/{locale}/collections/{collection_slug}", rateLimit(cachedHelpCenterPage(handleShowHelpCenterCollection), "public")) + getAndHead("/hc/{slug}/{locale}/articles/{article_slug}", rateLimit(countArticleView(cachedHelpCenterPage(handleShowHelpCenterArticle)), "public")) g.GET("/csat/{uuid}", rateLimit(handleShowCSAT, "public")) g.GET("/csat/{uuid}/widget", rateLimit(handleShowCSATWidget, "public")) diff --git a/cmd/helpcenter.go b/cmd/helpcenter.go index 3d1e0d74..cadf4434 100644 --- a/cmd/helpcenter.go +++ b/cmd/helpcenter.go @@ -6,6 +6,7 @@ import ( "fmt" "hash/fnv" "html/template" + "io" "log" "math" "net" @@ -25,6 +26,7 @@ import ( realip "github.com/ferluci/fast-realip" "github.com/knadh/stuffbin" "github.com/valyala/fasthttp" + "github.com/zerodha/fastcache/v4" "github.com/zerodha/fastglue" ) @@ -43,7 +45,15 @@ const ( sitemapDate = "2006-01-02" schemaOrgContext = "https://schema.org" - helpCenterCacheControl = "public, max-age=300, stale-while-revalidate=3600" + helpCenterCacheControl = "public, no-cache" + + fastCachePrefix = "libredesk:cache:" + + helpCenterCacheTTL = 24 * time.Hour + + helpCenterCacheGroup = "helpcenter" + + helpCenterXMLCacheControl = "public, max-age=300, stale-while-revalidate=3600" // articleFeedbackDedupTTL is how long a reader IP's vote on an article suppresses further votes. articleFeedbackDedupTTL = 24 * time.Hour @@ -60,6 +70,16 @@ const ( ) var ( + // Logger is set because fastcache writes to the field when it finds it nil, racing across requests. + helpCenterCacheOpts = &fastcache.Options{ + NamespaceKey: "slug", + ETag: true, + IncludeQueryString: true, + // Backstop for keys nothing invalidates, such as a deleted help center's. + TTL: helpCenterCacheTTL, + Logger: log.New(io.Discard, "", 0), + } + // crawlerUARe matches search and preview bots, whose hits must not count as reader views. crawlerUARe = regexp.MustCompile(`(?i)bot\b|bot/|crawler|spider|crawling|slurp|facebookexternalhit|preview|headlesschrome|lighthouse|feedfetcher|python-requests|curl/|wget/`) @@ -689,9 +709,6 @@ func handleShowHelpCenterArticle(r *fastglue.Request) error { } // The JSON-LD embeds the author too, not just the byline. hideArticleAuthor(helpCenterTheme(helpCenter), &article) - if !isCrawler(r) { - app.helpcenter.IncrementArticleViewCount(article.ID) - } if markdown { r.RequestCtx.Response.Header.Set("X-Robots-Tag", noIndexHeader) @@ -1055,11 +1072,13 @@ func handleUpdateArticle(r *fastglue.Request) error { // no-store default that RenderWebPage applies for the app's authenticated pages. func renderHelpCenterPage(r *fastglue.Request, name string, data map[string]interface{}) error { app := r.Context.(*App) - err := app.tmpl.RenderWebPage(r.RequestCtx, name, data) + if err := app.tmpl.RenderWebPage(r.RequestCtx, name, data); err != nil { + return err + } r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl) r.RequestCtx.Response.Header.Del("Pragma") r.RequestCtx.Response.Header.Del("Expires") - return err + return nil } // sendXML writes v as an XML document. @@ -1069,12 +1088,77 @@ func sendXML(r *fastglue.Request, v any) error { return sendErrorEnvelope(r, err) } r.RequestCtx.SetContentType("application/xml; charset=utf-8") - r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl) + r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterXMLCacheControl) fmt.Fprint(r.RequestCtx, xml.Header) r.RequestCtx.Write(out) return nil } +// countArticleView records a view after the page is served, counting cache hits too. +func countArticleView(h fastglue.FastRequestHandler) fastglue.FastRequestHandler { + return func(r *fastglue.Request) error { + err := h(r) + switch r.RequestCtx.Response.StatusCode() { + case fasthttp.StatusOK, fasthttp.StatusNotModified: + if !isCrawler(r) { + app := r.Context.(*App) + slug, _ := r.RequestCtx.UserValue("slug").(string) + articleSlug, _ := r.RequestCtx.UserValue("article_slug").(string) + locale, _ := r.RequestCtx.UserValue("locale").(string) + app.helpcenter.IncrementPublishedArticleView(slug, strings.TrimSuffix(articleSlug, markdownSlugExtension), locale) + } + } + return err + } +} + +// cachedHelpCenterPage serves a public help center page from the response cache, which +// stores the body alone, so the caching headers are re-applied on the hit and 304 paths. +func cachedHelpCenterPage(h fastglue.FastRequestHandler) fastglue.FastRequestHandler { + return func(r *fastglue.Request) error { + app := r.Context.(*App) + err := app.fastCache.Cached(h, helpCenterCacheOpts, helpCenterCacheGroup)(r) + switch r.RequestCtx.Response.StatusCode() { + case fasthttp.StatusOK, fasthttp.StatusNotModified: + r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl) + r.RequestCtx.Response.Header.Del("Pragma") + r.RequestCtx.Response.Header.Del("Expires") + } + return err + } +} + +// clearsHelpCenterCache drops the cached public pages after a successful write. +func clearsHelpCenterCache(h fastglue.FastRequestHandler) fastglue.FastRequestHandler { + return func(r *fastglue.Request) error { + err := h(r) + if r.RequestCtx.Response.StatusCode() < fasthttp.StatusMultipleChoices { + clearHelpCenterCache(r.Context.(*App), "") + } + return err + } +} + +// clearHelpCenterCache drops every cached public page for slug, or for all when slug is empty. +func clearHelpCenterCache(app *App, slug string) { + if slug != "" { + if err := app.fastCache.DelGroup(slug, helpCenterCacheGroup); err != nil { + app.lo.Error("error clearing help center cache", "slug", slug, "error", err) + } + return + } + helpCenters, err := app.helpcenter.GetAllHelpCenters() + if err != nil { + app.lo.Error("error listing help centers to clear cache", "error", err) + return + } + for _, hc := range helpCenters { + if err := app.fastCache.DelGroup(hc.Slug, helpCenterCacheGroup); err != nil { + app.lo.Error("error clearing help center cache", "slug", hc.Slug, "error", err) + } + } +} + // isCrawler reports whether the request came from a bot rather than a reader; HEAD probes never count as readers. func isCrawler(r *fastglue.Request) bool { return r.RequestCtx.IsHead() || crawlerUARe.Match(r.RequestCtx.Request.Header.UserAgent()) diff --git a/cmd/helpcenter_cache_test.go b/cmd/helpcenter_cache_test.go new file mode 100644 index 00000000..874dfef2 --- /dev/null +++ b/cmd/helpcenter_cache_test.go @@ -0,0 +1,138 @@ +package main + +import ( + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/redis/go-redis/v9" + "github.com/valyala/fasthttp" + goredis "github.com/zerodha/fastcache/stores/goredis/v9" + "github.com/zerodha/fastcache/v4" + "github.com/zerodha/fastglue" +) + +func TestHelpCenterPageCacheServesAndInvalidates(t *testing.T) { + fc := newTestFastCache(t) + + renders := 0 + body := "

original

" + handler := func(r *fastglue.Request) error { + renders++ + r.RequestCtx.SetContentType("text/html; charset=utf-8") + r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl) + r.RequestCtx.Response.SetBodyString(body) + return nil + } + cached := fc.Cached(handler, helpCenterCacheOpts, helpCenterCacheGroup) + + const uri = "/hc/support/en/articles/refunds" + + first := request(uri, "support", "") + if err := cached(first); err != nil { + t.Fatalf("first request: %v", err) + } + if renders != 1 { + t.Fatalf("first request should render, renders = %d", renders) + } + etag := string(first.RequestCtx.Response.Header.Peek("ETag")) + if etag == "" { + t.Fatal("expected an ETag on the rendered page") + } + + second := request(uri, "support", "") + if err := cached(second); err != nil { + t.Fatalf("second request: %v", err) + } + if renders != 1 { + t.Errorf("second request re-rendered instead of using the cache, renders = %d", renders) + } + if got := string(second.RequestCtx.Response.Body()); got != body { + t.Errorf("cached body = %q, want %q", got, body) + } + + revalidate := request(uri, "support", etag) + if err := cached(revalidate); err != nil { + t.Fatalf("revalidation: %v", err) + } + if got := revalidate.RequestCtx.Response.StatusCode(); got != fasthttp.StatusNotModified { + t.Errorf("matching ETag returned %d, want 304", got) + } + if got := revalidate.RequestCtx.Response.Body(); len(got) != 0 { + t.Errorf("304 carried a body of %d bytes", len(got)) + } + + // An admin write clears the group, so the next read must re-render the edit. + if err := fc.DelGroup("support", helpCenterCacheGroup); err != nil { + t.Fatalf("clearing the group: %v", err) + } + body = "

edited

" + afterEdit := request(uri, "support", "") + if err := cached(afterEdit); err != nil { + t.Fatalf("request after edit: %v", err) + } + if renders != 2 { + t.Errorf("cleared cache did not re-render, renders = %d", renders) + } + if got := string(afterEdit.RequestCtx.Response.Body()); got != body { + t.Errorf("stale body served after an edit: got %q, want %q", got, body) + } + if newETag := string(afterEdit.RequestCtx.Response.Header.Peek("ETag")); newETag == etag { + t.Error("ETag survived an edit, readers holding the old copy would never refetch") + } +} + +func TestHelpCenterPageCacheIsolatesHelpCentersAndQueries(t *testing.T) { + fc := newTestFastCache(t) + + served := map[string]int{} + handler := func(r *fastglue.Request) error { + uri := string(r.RequestCtx.URI().RequestURI()) + served[uri]++ + r.RequestCtx.Response.SetBodyString(uri) + return nil + } + cached := fc.Cached(handler, helpCenterCacheOpts, helpCenterCacheGroup) + + for _, req := range []*fastglue.Request{ + request("/hc/support/en/search?q=refund", "support", ""), + request("/hc/support/en/search?q=billing", "support", ""), + request("/hc/docs/en/search?q=refund", "docs", ""), + } { + if err := cached(req); err != nil { + t.Fatalf("request: %v", err) + } + } + if len(served) != 3 { + t.Fatalf("expected 3 distinct cache entries, got %d: %v", len(served), served) + } + + // Clearing one help center must not touch another's pages. + if err := fc.DelGroup("support", helpCenterCacheGroup); err != nil { + t.Fatalf("clearing the group: %v", err) + } + if err := cached(request("/hc/docs/en/search?q=refund", "docs", "")); err != nil { + t.Fatalf("request: %v", err) + } + if got := served["/hc/docs/en/search?q=refund"]; got != 1 { + t.Errorf("clearing 'support' evicted 'docs', renders = %d", got) + } +} + +// newTestFastCache returns a cache backed by an in-memory redis. +func newTestFastCache(t *testing.T) *fastcache.FastCache { + t.Helper() + mr := miniredis.RunT(t) + return fastcache.New(goredis.New(goredis.Config{Prefix: fastCachePrefix}, redis.NewClient(&redis.Options{Addr: mr.Addr()}))) +} + +// request builds a GET for the given URI with the slug namespace the options read. +func request(uri, slug, ifNoneMatch string) *fastglue.Request { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.SetRequestURI(uri) + ctx.SetUserValue("slug", slug) + if ifNoneMatch != "" { + ctx.Request.Header.Set("If-None-Match", ifNoneMatch) + } + return &fastglue.Request{RequestCtx: ctx} +} diff --git a/cmd/init.go b/cmd/init.go index d4e21772..d717ac78 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -70,6 +70,8 @@ import ( "github.com/redis/go-redis/v9" flag "github.com/spf13/pflag" "github.com/volatiletech/null/v9" + goredis "github.com/zerodha/fastcache/stores/goredis/v9" + "github.com/zerodha/fastcache/v4" "github.com/zerodha/logf" ) @@ -940,6 +942,11 @@ func initRedis() *redis.Client { }) } +// initFastCache inits the shared HTTP response cache. +func initFastCache(rdb *redis.Client) *fastcache.FastCache { + return fastcache.New(goredis.New(goredis.Config{Prefix: fastCachePrefix}, rdb)) +} + // initRedis inits postgres DB. func initDB() *sqlx.DB { dsn := fmt.Sprintf( diff --git a/cmd/main.go b/cmd/main.go index 95f5a1ac..98520aee 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -57,6 +57,7 @@ import ( "github.com/knadh/koanf/v2" "github.com/knadh/stuffbin" "github.com/valyala/fasthttp" + "github.com/zerodha/fastcache/v4" "github.com/zerodha/fastglue" "github.com/zerodha/logf" ) @@ -128,6 +129,7 @@ type App struct { contextLink *contextlink.Manager rateLimit *ratelimit.Limiter redis *redis.Client + fastCache *fastcache.FastCache importer *importer.Importer wsHub *ws.Hub @@ -322,6 +324,7 @@ func main() { contextLink: initContextLink(db, i18n), rateLimit: rateLimiter, redis: rdb, + fastCache: initFastCache(rdb), userNotification: userNotification, wsHub: wsHub, } diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 036e7cfa..06e10dbb 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -92,7 +92,7 @@ In light mode `card` is pure white against a near-white background. In dark mode ## 3. Typography -Font: Instrument Sans. Sizes: `text-xs` 12 · `text-sm` 14 · `text-base` 16 · `text-lg` 18 · +Font: Geist. Sizes: `text-xs` 12 · `text-sm` 14 · `text-base` 16 · `text-lg` 18 · `text-xl` 20 · `text-2xl` 24 · `text-3xl` 30. Weights: 400 body · 500 labels · 600 headings · 700 rare emphasis. diff --git a/frontend/apps/main/index.html b/frontend/apps/main/index.html index 9361f948..781f17db 100644 --- a/frontend/apps/main/index.html +++ b/frontend/apps/main/index.html @@ -5,10 +5,8 @@ - - - + + diff --git a/frontend/apps/main/src/features/conversation/sidebar/ConversationSideBar.vue b/frontend/apps/main/src/features/conversation/sidebar/ConversationSideBar.vue index 5df27d1c..52c5fa83 100644 --- a/frontend/apps/main/src/features/conversation/sidebar/ConversationSideBar.vue +++ b/frontend/apps/main/src/features/conversation/sidebar/ConversationSideBar.vue @@ -18,7 +18,7 @@
{ const priorityOptions = computed(() => conversationStore.priorityOptions) +const agentOptions = computed(() => { + const none = { value: 'none', label: t('globals.terms.none') } + const isMe = (option) => String(option.value) === String(userStore.userID) + const me = usersStore.options.find(isMe) + if (!me) return [none, ...usersStore.options] + return [none, me, ...usersStore.options.filter((option) => !isMe(option))] +}) + const fetchTags = async () => { await tagStore.fetchTags() tags.value = tagStore.tags.map((item) => item.name) diff --git a/frontend/apps/widget/index.html b/frontend/apps/widget/index.html index 59fe1f2b..66ac7efe 100644 --- a/frontend/apps/widget/index.html +++ b/frontend/apps/widget/index.html @@ -6,10 +6,8 @@ libredesk Chat Widget - - - + + diff --git a/frontend/shared-ui/assets/styles/main.scss b/frontend/shared-ui/assets/styles/main.scss index db785c53..125a12e2 100644 --- a/frontend/shared-ui/assets/styles/main.scss +++ b/frontend/shared-ui/assets/styles/main.scss @@ -22,7 +22,7 @@ @layer base { html, body { - font-family: 'Instrument Sans', sans-serif; + @apply font-sans; min-height: 100%; overflow: hidden; margin: 0; diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs index f0da9695..3c8aef7a 100644 --- a/frontend/tailwind.config.cjs +++ b/frontend/tailwind.config.cjs @@ -1,6 +1,7 @@ const animate = require("tailwindcss-animate") const typography = require("@tailwindcss/typography") const plugin = require("tailwindcss/plugin") +const defaultTheme = require("tailwindcss/defaultTheme") const canHover = plugin(({ addVariant }) => addVariant("can-hover", "@media (hover: hover)")) @@ -26,6 +27,9 @@ module.exports = { } }, extend: { + fontFamily: { + sans: ['Geist', ...defaultTheme.fontFamily.sans] + }, height: { screen: '100dvh' }, diff --git a/go.mod b/go.mod index efcd19f1..1855f052 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/abhinavxd/ssrfguard v0.1.0 - github.com/alicebob/miniredis/v2 v2.32.1 + github.com/alicebob/miniredis/v2 v2.33.0 github.com/coreos/go-oidc/v3 v3.11.0 github.com/disintegration/imaging v1.6.2 github.com/emersion/go-imap/v2 v2.0.0-beta.3 @@ -42,6 +42,8 @@ require ( github.com/valyala/fasthttp v1.62.0 github.com/volatiletech/null/v9 v9.0.0 github.com/yuin/goldmark v1.8.4 + github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0 + github.com/zerodha/fastcache/v4 v4.2.1 github.com/zerodha/fastglue v1.8.0 github.com/zerodha/logf v0.5.5 github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0 diff --git a/go.sum b/go.sum index 8a694217..ad4cc84a 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,19 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/abhinavxd/ssrfguard v0.1.0 h1:Ns/llAQ63uGFehxSvhCd+WGDKmBEEmIH+E1AW1CGgWM= github.com/abhinavxd/ssrfguard v0.1.0/go.mod h1:eNVubb+m/r3KrKWYdG6hxzeAfj+t2ZmZss4V/x7D6Ws= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo= -github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw= +github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= +github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= @@ -19,19 +24,26 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -39,8 +51,16 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emersion/go-imap/v2 v2.0.0-beta.3 h1:z0TLMfYnDsFupXLhzRXgOzXenD3uPvNniQSu5fN1teg= github.com/emersion/go-imap/v2 v2.0.0-beta.3/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= @@ -54,6 +74,8 @@ github.com/fasthttp/websocket v1.5.9 h1:9deGuzYcCRKjk940kNwSN6Hd14hk4zYwropm4UsU github.com/fasthttp/websocket v1.5.9/go.mod h1:NLzHBFur260OMuZHohOfYQwMTpR7sfSpUnuqKxMpgKA= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferluci/fast-realip v1.0.1 h1:zPi0iv7zgOOlM/qJt9mozLz5IjVRAezaqHJoQ0JJ/yI= github.com/ferluci/fast-realip v1.0.1/go.mod h1:Ag7xdRQ9GOCL/pwbDe4zJv6SlfYdROArc8O+qIKhRc4= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -62,6 +84,12 @@ github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -71,6 +99,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= @@ -131,8 +161,12 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mackee/go-readability v0.3.1 h1:DUwcwlhNLPtrBkGyJPcKp51oOKBvZvvMDPPFFLUIcKc= github.com/mackee/go-readability v0.3.1/go.mod h1:lfyLr0PJ+fQ+z6r6IBrexFxP4AoVsaDJAGvMcoJ4UAM= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= @@ -148,6 +182,18 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-karan/balance v0.0.0-20250317053523-d32c6ade6cf1 h1:YYLFUQMdeCyUVUAxE/IkPbkDfOVkUl9mbBdwkzU4UbE= github.com/mr-karan/balance v0.0.0-20250317053523-d32c6ade6cf1/go.mod h1:YMjMm+2l1ye+v1MeuUJ1QPxXKzWp+x8iqg4vWuKB3Ao= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= @@ -158,6 +204,10 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -168,6 +218,8 @@ github.com/pkoukk/tiktoken-go-loader v0.0.2 h1:LUKws63GV3pVHwH1srkBplBv+7URgmOmh github.com/pkoukk/tiktoken-go-loader v0.0.2/go.mod h1:4mIkYyZooFlnenDlormIo6cd5wrlUKNr97wp9nGgEKo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM= github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rhnvrm/simples3 v0.10.1 h1:V/EbG6tC3F5MS7Vw5D02qIbNJom2jU+FVtGdrmsD67o= @@ -175,6 +227,12 @@ github.com/rhnvrm/simples3 v0.10.1/go.mod h1:c2xW30bukipkBlWNnXG1wDjq3gykQ6ww2AB github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= @@ -185,6 +243,12 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= @@ -203,6 +267,12 @@ github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0 h1:DtP3AflFjGTyHjXgd3UxuFfvGqk0rmfzm9PRbuiYwjU= +github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0/go.mod h1:Pds6hwIUaduCzChTTS3JWKdXLhpdvjvF3r8rgB0huYk= +github.com/zerodha/fastcache/v4 v4.2.1 h1:MbkZv/lwSgAqoCnP+b023wxNlqHch0CU/B6RXaLgZKI= +github.com/zerodha/fastcache/v4 v4.2.1/go.mod h1:jfFLkiuyMIO8u7KlojdeLGaVyPuVXES0PRGxS7byr6s= github.com/zerodha/fastglue v1.8.0 h1:yCfb8YwZLoFrzHiojRcie19olLDT48vjuinVn1Ge5Uc= github.com/zerodha/fastglue v1.8.0/go.mod h1:+fB3j+iAz9Et56KapvdVoL79+m3h7NphR92TU4exWgk= github.com/zerodha/logf v0.5.5 h1:AhxHlixHNYwhFjvlgTv6uO4VBKYKxx2I6SbHoHtWLBk= @@ -211,6 +281,14 @@ github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0 h1:wkFp+/IM1cXjmidprZw8 github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0/go.mod h1:HxUlesaeO/JuymHHoPQ+7GKVjrkCwEYiM/0+oyiaaDo= github.com/zerodha/simplesessions/v3 v3.0.0 h1:seHwxVNnlCbp5nG8GFxSsRUdiHnfb39QdEW3J536O9Y= github.com/zerodha/simplesessions/v3 v3.0.0/go.mod h1:lAK+CJmZRlbvfq+OnkB8Iyf6LWgjzvUuWYKX1XA51P0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -240,7 +318,6 @@ golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/helpcenter/helpcenter.go b/internal/helpcenter/helpcenter.go index 784e1440..a721faa7 100644 --- a/internal/helpcenter/helpcenter.go +++ b/internal/helpcenter/helpcenter.go @@ -203,6 +203,7 @@ type queries struct { GetPublishedArticlesByCollection *sqlx.Stmt `query:"get-published-articles-by-collection"` SearchPublishedArticles *sqlx.Stmt `query:"search-published-articles"` IncrementArticleViewCount *sqlx.Stmt `query:"increment-article-view-count"` + IncrementPublishedArticleView *sqlx.Stmt `query:"increment-published-article-view-count"` InsertArticleFeedback *sqlx.Stmt `query:"insert-article-feedback"` InsertSearchQuery *sqlx.Stmt `query:"insert-search-query"` @@ -957,6 +958,13 @@ func (m *Manager) IncrementArticleViewCount(id int) { } } +// IncrementPublishedArticleView increments a published article's view count by its URL parts. +func (m *Manager) IncrementPublishedArticleView(helpCenterSlug, articleSlug, locale string) { + if _, err := m.q.IncrementPublishedArticleView.Exec(helpCenterSlug, articleSlug, locale); err != nil { + m.lo.Error("error incrementing article view count", "error", err, "slug", articleSlug) + } +} + // RecordArticleFeedback stores a reader's helpful/not-helpful vote for a published article. func (m *Manager) RecordArticleFeedback(articleID int, isHelpful bool) error { if _, err := m.q.InsertArticleFeedback.Exec(articleID, isHelpful); err != nil { diff --git a/internal/helpcenter/queries.sql b/internal/helpcenter/queries.sql index 8aad9891..5469796f 100644 --- a/internal/helpcenter/queries.sql +++ b/internal/helpcenter/queries.sql @@ -424,6 +424,13 @@ UPDATE help_articles SET view_count = view_count + 1 WHERE id = $1; +-- name: increment-published-article-view-count +UPDATE help_articles a +SET view_count = view_count + 1 +FROM article_collections c, help_centers h +WHERE a.collection_id = c.id AND c.help_center_id = h.id + AND h.slug = $1 AND a.slug = $2 AND a.locale = $3 AND a.status = 'published'; + -- name: insert-article-feedback INSERT INTO help_article_feedback (article_id, is_helpful) SELECT $1, $2 diff --git a/static/email-templates/base.html b/static/email-templates/base.html index c22e6c47..5e8e40be 100644 --- a/static/email-templates/base.html +++ b/static/email-templates/base.html @@ -6,14 +6,10 @@ - - -