mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-11-01 17:09:52 +00:00
1cda45797e
Preserve existing template fields while sharing conversation data across customer emails and agent alerts. Use the system layout for automation alerts and add regression coverage for notification delivery and conversation switching.
218 lines
6.9 KiB
Go
218 lines
6.9 KiB
Go
package template
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"text/template"
|
|
|
|
cmodels "github.com/abhinavxd/libredesk/internal/conversation/models"
|
|
umodels "github.com/abhinavxd/libredesk/internal/user/models"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
const (
|
|
// Built-in templates names stored in the database.
|
|
TmplConversationAssigned = "Conversation assigned"
|
|
TmplSLABreachWarning = "SLA breach warning"
|
|
TmplSLABreached = "SLA breached"
|
|
TmplMentioned = "Mentioned in conversation"
|
|
TmplCSATRequest = "CSAT request"
|
|
TmplNewReply = "New reply from contact"
|
|
TmplNewReplyParticipating = "New reply on participating conversation"
|
|
TmplConversationReopened = "Conversation reopened"
|
|
|
|
// Built-in templates fetched from memory stored in `static` directory.
|
|
TmplResetPassword = "reset-password"
|
|
TmplWelcome = "welcome"
|
|
|
|
// Template names for rendering.
|
|
TmplBase = "base"
|
|
TmplContent = "content"
|
|
)
|
|
|
|
// RenderString renders Go template variables in the given content string
|
|
// without wrapping it in the base email template. Returns original content on any error.
|
|
func (m *Manager) RenderString(data any, content string) string {
|
|
t, err := template.New("content").Funcs(m.funcMap).Parse(content)
|
|
if err != nil {
|
|
return content
|
|
}
|
|
var buf strings.Builder
|
|
if err := t.Execute(&buf, data); err != nil {
|
|
return content
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// RenderStoredTemplate fetches a template by name and renders its body with the provided data
|
|
// without wrapping it in the base email template.
|
|
func (m *Manager) RenderStoredTemplate(name string, data any) (string, error) {
|
|
tmpl, err := m.getByName(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return m.RenderString(data, tmpl.Body), nil
|
|
}
|
|
|
|
// RenderEmailWithTemplate renders content inside the default outgoing email template.
|
|
func (m *Manager) RenderEmailWithTemplate(data any, content string) (string, error) {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
defaultTmpl, err := m.getDefaultOutgoingEmailTemplate()
|
|
if err != nil {
|
|
m.lo.Error("error fetching default outgoing email template", "error", err)
|
|
}
|
|
|
|
if defaultTmpl.Body == "" {
|
|
defaultTmpl.Body = `{{ template "content" . }}`
|
|
}
|
|
|
|
baseTemplate, err := template.New(TmplBase).Funcs(m.funcMap).Parse(defaultTmpl.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing base template: %w", err)
|
|
}
|
|
|
|
contentTemplate, err := template.New(TmplContent).Funcs(m.funcMap).Parse(content)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing content template: %w", err)
|
|
}
|
|
|
|
baseTemplate, err = baseTemplate.AddParseTree(TmplContent, contentTemplate.Tree)
|
|
if err != nil {
|
|
return "", fmt.Errorf("adding content template: %w", err)
|
|
}
|
|
|
|
var rendered strings.Builder
|
|
if err := baseTemplate.ExecuteTemplate(&rendered, TmplBase, data); err != nil {
|
|
return "", fmt.Errorf("executing base template: %w", err)
|
|
}
|
|
|
|
return rendered.String(), nil
|
|
}
|
|
|
|
func (m *Manager) RenderStoredEmailTemplate(name string, data any) (string, string, error) {
|
|
tmpl, err := m.getByName(name)
|
|
if err != nil {
|
|
if err == ErrTemplateNotFound {
|
|
return "", "", fmt.Errorf("template %s not found", name)
|
|
}
|
|
return "", "", err
|
|
}
|
|
|
|
executeSubjectTemplate := func(subject string) (string, error) {
|
|
var sb strings.Builder
|
|
subjectTmpl, err := template.New("subject").Funcs(m.funcMap).Parse(subject)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing subject template: %w", err)
|
|
}
|
|
if err := subjectTmpl.Execute(&sb, data); err != nil {
|
|
return "", fmt.Errorf("executing subject template: %w", err)
|
|
}
|
|
return sb.String(), nil
|
|
}
|
|
|
|
content, err := m.RenderNotificationEmail(tmpl.Body, data)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
subject, err := executeSubjectTemplate(tmpl.Subject.String)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return content, subject, nil
|
|
}
|
|
|
|
// RenderInMemoryTemplate executes an in-memory template with data and returns the rendered content.
|
|
// This is for system emails like reset password and welcome email etc.
|
|
func (m *Manager) RenderInMemoryTemplate(name string, data interface{}) (string, error) {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
var buf bytes.Buffer
|
|
if err := m.tpls.ExecuteTemplate(&buf, name, data); err != nil {
|
|
return "", fmt.Errorf("executing in-memory template %q: %w", name, err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// RenderWebPage renders a template to the http.ResponseWriter with data.
|
|
func (m *Manager) RenderWebPage(ctx *fasthttp.RequestCtx, tmplFile string, data map[string]interface{}) error {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
ctx.SetContentType("text/html; charset=utf-8")
|
|
ctx.SetStatusCode(fasthttp.StatusOK)
|
|
// Add no-cache headers
|
|
ctx.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
|
ctx.Response.Header.Set("Pragma", "no-cache")
|
|
ctx.Response.Header.Set("Expires", "0")
|
|
return m.webTpls.ExecuteTemplate(ctx, tmplFile, data)
|
|
}
|
|
|
|
func (m *Manager) RenderNotificationEmail(content string, data any) (string, error) {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
if m.notificationTpls == nil {
|
|
return "", fmt.Errorf("notification templates are not loaded")
|
|
}
|
|
tmpl, err := m.notificationTpls.Clone()
|
|
if err != nil {
|
|
return "", fmt.Errorf("cloning notification templates: %w", err)
|
|
}
|
|
tmpl.Funcs(m.funcMap)
|
|
if _, err := tmpl.New(TmplContent).Parse(content); err != nil {
|
|
return "", fmt.Errorf("parsing notification content: %w", err)
|
|
}
|
|
tmpl, err = tmpl.New(TmplBase).Parse(`{{ template "header" . }}{{ template "content" . }}{{ template "footer" . }}`)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing notification layout: %w", err)
|
|
}
|
|
var rendered strings.Builder
|
|
if err := tmpl.ExecuteTemplate(&rendered, TmplBase, data); err != nil {
|
|
return "", fmt.Errorf("rendering notification email: %w", err)
|
|
}
|
|
return rendered.String(), nil
|
|
}
|
|
|
|
func BuildConversationData(conversation cmodels.Conversation, recipient, author umodels.User) map[string]any {
|
|
isAIComposed := author.Type == umodels.UserTypeAIAssistant
|
|
authorData := personData(author)
|
|
if author.IsSystemUser() {
|
|
authorData = personData(umodels.User{})
|
|
authorData["FullName"] = ""
|
|
}
|
|
return map[string]any{
|
|
"Conversation": map[string]any{
|
|
"ReferenceNumber": conversation.ReferenceNumber,
|
|
"Subject": conversation.Subject.String,
|
|
"Priority": conversation.Priority.String,
|
|
"UUID": conversation.UUID,
|
|
},
|
|
"Inbox": map[string]any{
|
|
"Name": conversation.InboxName,
|
|
"Channel": conversation.InboxChannel,
|
|
},
|
|
"Contact": map[string]any{
|
|
"FirstName": conversation.Contact.FirstName,
|
|
"LastName": conversation.Contact.LastName,
|
|
"FullName": conversation.Contact.FullName(),
|
|
"Email": conversation.Contact.Email.String,
|
|
},
|
|
"Recipient": personData(recipient),
|
|
"Author": authorData,
|
|
"IsAIComposed": isAIComposed,
|
|
}
|
|
}
|
|
|
|
func personData(person umodels.User) map[string]any {
|
|
return map[string]any{
|
|
"FirstName": person.FirstName,
|
|
"LastName": person.LastName,
|
|
"FullName": person.FullName(),
|
|
"Email": person.Email.String,
|
|
}
|
|
}
|