diff --git a/internal/comm/api.go b/internal/comm/api.go index 7381c65..73e3630 100644 --- a/internal/comm/api.go +++ b/internal/comm/api.go @@ -3,6 +3,7 @@ package comm import ( "github.com/eternal-flame-AD/yoake/internal/auth" "github.com/eternal-flame-AD/yoake/internal/comm/model" + "github.com/eternal-flame-AD/yoake/internal/util" "github.com/labstack/echo/v4" ) @@ -13,7 +14,7 @@ type CommStatusResponse struct { } `json:"communicators"` } -func (c *CommProvider) RegisterAPIRoute(g *echo.Group) { +func (c *Communicator) RegisterAPIRoute(g *echo.Group) { send := g.Group("/send", auth.RequireMiddleware(auth.RoleAdmin)) { send.POST("", func(ctx echo.Context) error { @@ -49,11 +50,11 @@ func (c *CommProvider) RegisterAPIRoute(g *echo.Group) { SupportedMIME []string }{ Method: comm, - SupportedMIME: c.communicators[comm].SupportedMIME(), + SupportedMIME: c.commMethods[comm].SupportedMIME(), }) } - for key, comm := range c.communicators { - if !contains(c.fallbackCommunicators, key) { + for key, comm := range c.commMethods { + if !util.Contain(c.fallbackCommunicators, key) { communicators = append(communicators, struct { Method string SupportedMIME []string diff --git a/internal/comm/communicator.go b/internal/comm/communicator.go new file mode 100644 index 0000000..50df296 --- /dev/null +++ b/internal/comm/communicator.go @@ -0,0 +1,108 @@ +package comm + +import ( + "errors" + "fmt" + "log" + + "github.com/eternal-flame-AD/yoake/internal/comm/email" + "github.com/eternal-flame-AD/yoake/internal/comm/gotify" + "github.com/eternal-flame-AD/yoake/internal/comm/model" + "github.com/eternal-flame-AD/yoake/internal/util" +) + +type Communicator struct { + commMethods map[string]model.CommMethod + fallbackCommunicators []string +} + +var ( + errMethodNotSupported = errors.New("method not supported") +) + +func (c *Communicator) actualSendGenericMessage(tryMethod string, message model.GenericMessage) error { + if comm, ok := c.commMethods[tryMethod]; ok { + if convertedMsg, err := ConvertGenericMessage(&message, comm.SupportedMIME()); err == nil { + return comm.SendGenericMessage(*convertedMsg) + } else { + return err + } + } + return errMethodNotSupported +} + +// GetMethod returns the method with the given name. +func (c *Communicator) GetMethod(method string) model.CommMethod { + return c.commMethods[method] +} + +// GetMethodsByMIME returns a list of methods that support the given MIME type as the message type, MIME convertions were considered. +func (c *Communicator) GetMethodsByMIME(mime string) []model.CommMethod { + var result []model.CommMethod + for _, comm := range c.commMethods { + if util.Contain(ConvertOutMIMEToSupportedInMIME(comm.SupportedMIME()), mime) { + result = append(result, comm) + } + } + return result +} + +type ErrorSentWithFallback struct { + OriginalError error + OrignalMethod string + FallbackMethod string +} + +func (e ErrorSentWithFallback) Error() string { + return fmt.Sprintf("used fallback method %s because original method %s reeported error: %v", e.FallbackMethod, e.OrignalMethod, e.OriginalError) +} + +// SendGenericMethods sends a message using the preferred method +// if the preferred method failed to send the message, fallback methods will be tried, +// and an ErrorSentWithFabback will be returned if any fallback method succeeded. +// if fallback methods failed as well the original error will be returned. +func (c *Communicator) SendGenericMessage(preferredMethod string, message model.GenericMessage) error { + if preferredMethod == "" { + preferredMethod = c.fallbackCommunicators[0] + } + if origErr := c.actualSendGenericMessage(preferredMethod, message); origErr != nil { + log.Printf("Failed to send message using preferred method %s: %v. trying fallback methods", preferredMethod, origErr) + for _, fallback := range c.fallbackCommunicators { + if fallback == preferredMethod { + continue + } + if err := c.actualSendGenericMessage(fallback, message); err == nil { + log.Printf("Sent message using fallback method %s", fallback) + return ErrorSentWithFallback{ + OriginalError: origErr, + OrignalMethod: preferredMethod, + FallbackMethod: fallback, + } + } else { + log.Printf("Failed to send message using fallback method %s: %v", fallback, err) + } + } + return origErr + } + return nil +} + +func InitCommunicator() *Communicator { + comm := &Communicator{ + commMethods: make(map[string]model.CommMethod), + } + if emailHandler, err := email.NewHandler(); err == nil { + comm.commMethods["email"] = emailHandler + comm.fallbackCommunicators = append(comm.fallbackCommunicators, "email") + } else { + log.Printf("Failed to initialize email communicator: %v", err) + } + if gotifyHandler, err := gotify.NewClient(); err == nil { + comm.commMethods["gotify"] = gotifyHandler + comm.fallbackCommunicators = append(comm.fallbackCommunicators, "gotify") + } else { + log.Printf("Failed to initialize gotify communicator: %v", err) + } + + return comm +} diff --git a/internal/comm/convert.go b/internal/comm/convert.go index 2cdd5f5..3c8450f 100644 --- a/internal/comm/convert.go +++ b/internal/comm/convert.go @@ -12,16 +12,18 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/eternal-flame-AD/yoake/internal/comm/model" "github.com/eternal-flame-AD/yoake/internal/servetpl/funcmap" + "github.com/eternal-flame-AD/yoake/internal/util" "github.com/gomarkdown/markdown" ) -func contains[T comparable](s []T, e T) bool { - for _, a := range s { - if a == e { - return true +func unique[T comparable](s []T) []T { + var result []T + for _, e := range s { + if !util.Contain(result, e) { + result = append(result, e) } } - return false + return result } type ErrorMIMENoOverlap struct { @@ -33,8 +35,25 @@ func (e *ErrorMIMENoOverlap) Error() string { return fmt.Sprintf("message MIME type %s is not supported by this communicator. Supported MIME types are: %s", e.MessageMIME, e.supportedMIME) } +func ConvertOutMIMEToSupportedInMIME(outMIMEs []string) (inMIMEs []string) { + inMIMEs = outMIMEs + for _, out := range outMIMEs { + if out == "text/plain" { + inMIMEs = append(inMIMEs, "text/html", "text/markdown") + } + if out == "text/html" { + inMIMEs = append(inMIMEs, "text/markdown") + } + } + for _, in := range inMIMEs { + inMIMEs = append(inMIMEs, in+"+html/template", in+"+text/template") + } + inMIMEs = unique(inMIMEs) + return +} + func ConvertGenericMessage(msgOrig *model.GenericMessage, supportedMIMES []string) (*model.GenericMessage, error) { - if contains(supportedMIMES, msgOrig.MIME) { + if util.Contain(supportedMIMES, msgOrig.MIME) { return msgOrig, nil } msg := *msgOrig @@ -86,17 +105,17 @@ func ConvertGenericMessage(msgOrig *model.GenericMessage, supportedMIMES []strin } msg.Body = output.String() } - if contains(supportedMIMES, msg.MIME) { + if util.Contain(supportedMIMES, msg.MIME) { return &msg, nil } // convert markdown to html - if msg.MIME == "text/markdown" && !contains(supportedMIMES, "text/markdown") { + if msg.MIME == "text/markdown" && !util.Contain(supportedMIMES, "text/markdown") { msg.Body = string(markdown.ToHTML([]byte(msg.Body), nil, nil)) msg.MIME = "text/html" } // convert html to text - if msg.MIME == "text/html" && !contains(supportedMIMES, "text/html") && contains(supportedMIMES, "text/plain") { + if msg.MIME == "text/html" && !util.Contain(supportedMIMES, "text/html") && util.Contain(supportedMIMES, "text/plain") { docBuf := strings.NewReader(msg.Body) doc, err := goquery.NewDocumentFromReader(docBuf) if err != nil { @@ -106,7 +125,7 @@ func ConvertGenericMessage(msgOrig *model.GenericMessage, supportedMIMES []strin msg.MIME = "text/plain" } - if !contains(supportedMIMES, msg.MIME) { + if !util.Contain(supportedMIMES, msg.MIME) { return nil, &ErrorMIMENoOverlap{ MessageMIME: msg.MIME, supportedMIME: supportedMIMES, diff --git a/internal/comm/interface.go b/internal/comm/interface.go deleted file mode 100644 index 30804d0..0000000 --- a/internal/comm/interface.go +++ /dev/null @@ -1,15 +0,0 @@ -package comm - -import ( - "github.com/eternal-flame-AD/yoake/internal/comm/model" - "github.com/labstack/echo/v4" -) - -type Communicator interface { - SupportedMIME() []string - SendGenericMessage(message model.GenericMessage) error -} - -type CommunicatorWithRoute interface { - RegisterRoute(g *echo.Group) -} diff --git a/internal/comm/model/generic.go b/internal/comm/model/generic.go index 9c436de..99540bf 100644 --- a/internal/comm/model/generic.go +++ b/internal/comm/model/generic.go @@ -1,9 +1,10 @@ package model type GenericMessage struct { - Subject string `json:"subject" form:"subject" query:"subject"` - Body string `json:"body" form:"body" query:"body"` - MIME string `json:"mime" form:"mime" query:"mime"` + Subject string `json:"subject" form:"subject" query:"subject"` + Body string `json:"body" form:"body" query:"body"` + MIME string `json:"mime" form:"mime" query:"mime"` + ThreadID uint64 `json:"thread_id" form:"thread_id" query:"thread_id"` Context interface{} } diff --git a/internal/comm/model/interface.go b/internal/comm/model/interface.go new file mode 100644 index 0000000..407c2b4 --- /dev/null +++ b/internal/comm/model/interface.go @@ -0,0 +1,20 @@ +package model + +import ( + "github.com/labstack/echo/v4" +) + +type CommMethod interface { + SupportedMIME() []string + SendGenericMessage(message GenericMessage) error +} + +type CommMethodWithRoute interface { + RegisterRoute(g *echo.Group) +} + +type Communicator interface { + GetMethod(method string) CommMethod + GetMethodsByMIME(mime string) []CommMethod + SendGenericMessage(preferredMethod string, message GenericMessage) error +} diff --git a/internal/comm/provider.go b/internal/comm/provider.go deleted file mode 100644 index 24f54a5..0000000 --- a/internal/comm/provider.go +++ /dev/null @@ -1,72 +0,0 @@ -package comm - -import ( - "errors" - "log" - - "github.com/eternal-flame-AD/yoake/internal/comm/email" - "github.com/eternal-flame-AD/yoake/internal/comm/gotify" - "github.com/eternal-flame-AD/yoake/internal/comm/model" -) - -type CommProvider struct { - communicators map[string]Communicator - fallbackCommunicators []string -} - -var ( - errMethodNotSupported = errors.New("method not supported") -) - -func (c *CommProvider) actualSendGenericMessage(tryMethod string, message model.GenericMessage) error { - if comm, ok := c.communicators[tryMethod]; ok { - if convertedMsg, err := ConvertGenericMessage(&message, comm.SupportedMIME()); err == nil { - return comm.SendGenericMessage(*convertedMsg) - } else { - return err - } - } - return errMethodNotSupported -} - -func (c *CommProvider) SendGenericMessage(preferredMethod string, message model.GenericMessage) error { - if preferredMethod == "" { - preferredMethod = c.fallbackCommunicators[0] - } - if err := c.actualSendGenericMessage(preferredMethod, message); err != nil { - log.Printf("Failed to send message using preferred method %s: %v. trying fallback methods", preferredMethod, err) - for _, fallback := range c.fallbackCommunicators { - if fallback == preferredMethod { - continue - } - if err := c.actualSendGenericMessage(fallback, message); err == nil { - log.Printf("Sent message using fallback method %s", fallback) - return nil - } else { - log.Printf("Failed to send message using fallback method %s: %v", fallback, err) - } - } - return err - } - return nil -} - -func InitializeCommProvider() *CommProvider { - comm := &CommProvider{ - communicators: make(map[string]Communicator), - } - if emailHandler, err := email.NewHandler(); err == nil { - comm.communicators["email"] = emailHandler - comm.fallbackCommunicators = append(comm.fallbackCommunicators, "email") - } else { - log.Printf("Failed to initialize email communicator: %v", err) - } - if gotifyHandler, err := gotify.NewClient(); err == nil { - comm.communicators["gotify"] = gotifyHandler - comm.fallbackCommunicators = append(comm.fallbackCommunicators, "gotify") - } else { - log.Printf("Failed to initialize gotify communicator: %v", err) - } - - return comm -} diff --git a/internal/util/slice.go b/internal/util/slice.go new file mode 100644 index 0000000..85dad67 --- /dev/null +++ b/internal/util/slice.go @@ -0,0 +1,33 @@ +package util + +// Contain checks if an element is in a slice. +func Contain[T comparable](a []T, x T) bool { + for _, n := range a { + if x == n { + return true + } + } + return false +} + +// Unique returns a slice with all duplicate elements removed. +func Unique[T comparable](a []T) []T { + var result []T + for _, e := range a { + if !Contain(result, e) { + result = append(result, e) + } + } + return result +} + +// AntiJOIN returns a slice with all elements in a that are not in b. +func AntiJoin[T comparable](a []T, b []T) []T { + var result []T + for _, e := range a { + if !Contain(b, e) { + result = append(result, e) + } + } + return result +} diff --git a/server/webroot/server.go b/server/webroot/server.go index 2760152..68b4ddf 100644 --- a/server/webroot/server.go +++ b/server/webroot/server.go @@ -23,7 +23,7 @@ import ( "github.com/labstack/echo/v4/middleware" ) -func Init(hostname string, comm *comm.CommProvider, database db.DB) { +func Init(hostname string, comm *comm.Communicator, database db.DB) { e := echo.New() webroot := config.Config().WebRoot