Ajout d'une newsletter basique #25
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/mail"
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/voter"
|
"forge.cadoles.com/Cadoles/daddy/internal/voter"
|
||||||
|
|
||||||
|
@ -108,5 +109,11 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con
|
||||||
model.NewWorkgroupVoter(),
|
model.NewWorkgroupVoter(),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
ctn.Provide(mail.ServiceName, mail.ServiceProvider(
|
||||||
|
mail.WithServer(conf.SMTP.Host, conf.SMTP.Port),
|
||||||
|
mail.WithCredentials(conf.SMTP.User, conf.SMTP.Password),
|
||||||
|
mail.WithTLS(conf.SMTP.UseStartTLS, conf.SMTP.InsecureSkipVerify),
|
||||||
|
))
|
||||||
|
|
||||||
return ctn, nil
|
return ctn, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,6 +153,8 @@ func main() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go runTaskScheduler(ctx, conf)
|
||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
// Define base middlewares
|
// Define base middlewares
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/task"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cronLogger struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *cronLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||||
|
fields := l.createFields(keysAndValues)
|
||||||
|
logger.Info(l.ctx, msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *cronLogger) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||||
|
fields := l.createFields(keysAndValues)
|
||||||
|
fields = append(fields, logger.E(err))
|
||||||
|
logger.Error(l.ctx, msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *cronLogger) createFields(keysAndValues ...interface{}) []logger.Field {
|
||||||
|
fields := make([]logger.Field, 0)
|
||||||
|
|
||||||
|
var key string
|
||||||
|
|
||||||
|
for _, v := range keysAndValues {
|
||||||
|
children, ok := v.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, vv := range children {
|
||||||
|
if i%2 == 0 {
|
||||||
|
key = fmt.Sprintf("%v", vv)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = append(fields, logger.F(key, vv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTaskScheduler(ctx context.Context, conf *config.Config) {
|
||||||
|
c := cron.New(
|
||||||
|
cron.WithLogger(&cronLogger{ctx}),
|
||||||
|
)
|
||||||
|
|
||||||
|
tasks := map[string]task.Task{
|
||||||
|
conf.Task.Newsletter.CronSpec: task.NewNewsletter(
|
||||||
|
ctx,
|
||||||
|
conf.Task.Newsletter.TimeRange,
|
||||||
|
conf.Task.Newsletter.BaseURL,
|
||||||
|
conf.Task.Newsletter.ContentTemplate,
|
||||||
|
conf.Task.Newsletter.SubjectTemplate,
|
||||||
|
conf.SMTP.SenderAddress,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
for spec, task := range tasks {
|
||||||
|
if _, err := c.AddFunc(spec, task.Run); err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not schedule task",
|
||||||
|
logger.F("task", task.Name()),
|
||||||
|
logger.E(errors.WithStack(err)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Start()
|
||||||
|
}
|
5
go.mod
5
go.mod
|
@ -4,6 +4,7 @@ go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032
|
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032
|
||||||
|
forge.cadoles.com/wpetit/hydra-passwordless v0.0.0-20200908094025-38ac4422dddc // indirect
|
||||||
github.com/99designs/gqlgen v0.11.3
|
github.com/99designs/gqlgen v0.11.3
|
||||||
github.com/antonmedv/expr v1.8.8
|
github.com/antonmedv/expr v1.8.8
|
||||||
github.com/caarlos0/env/v6 v6.2.2
|
github.com/caarlos0/env/v6 v6.2.2
|
||||||
|
@ -15,10 +16,14 @@ require (
|
||||||
github.com/jackc/pgx v3.6.2+incompatible
|
github.com/jackc/pgx v3.6.2+incompatible
|
||||||
github.com/jackc/pgx/v4 v4.7.1
|
github.com/jackc/pgx/v4 v4.7.1
|
||||||
github.com/jinzhu/gorm v1.9.14
|
github.com/jinzhu/gorm v1.9.14
|
||||||
|
github.com/lithammer/dedent v1.1.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/robfig/cron v1.2.0
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/rs/cors v1.7.0
|
github.com/rs/cors v1.7.0
|
||||||
github.com/vektah/gqlparser/v2 v2.0.1
|
github.com/vektah/gqlparser/v2 v2.0.1
|
||||||
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23
|
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2
|
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2
|
||||||
|
gopkg.in/mail.v2 v2.3.1
|
||||||
gopkg.in/yaml.v2 v2.2.8
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
)
|
)
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -16,6 +16,8 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032 h1:qTYaLPsLDlvqDkatONsvrisvfvpHaGe3lQqIaX7FFQQ=
|
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032 h1:qTYaLPsLDlvqDkatONsvrisvfvpHaGe3lQqIaX7FFQQ=
|
||||||
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032/go.mod h1:gkfqGyk7fCj2Z0ngEOCJ3K0FVmqft/8dFV/OnYT1vec=
|
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032/go.mod h1:gkfqGyk7fCj2Z0ngEOCJ3K0FVmqft/8dFV/OnYT1vec=
|
||||||
|
forge.cadoles.com/wpetit/hydra-passwordless v0.0.0-20200908094025-38ac4422dddc h1:9gc/1qizPtK6/iMVlizknWUFii75ntl2xSUV/FSC92Y=
|
||||||
|
forge.cadoles.com/wpetit/hydra-passwordless v0.0.0-20200908094025-38ac4422dddc/go.mod h1:nANHORi270d5jDXjeJ7B3pMgK9R4J0/17p1IIc+rhOk=
|
||||||
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
|
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
|
||||||
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
|
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
@ -48,6 +50,7 @@ github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9Pq
|
||||||
github.com/antonmedv/expr v1.8.8 h1:uVwIkIBNO2yn4vY2u2DQUqXTmv9jEEMCEcHa19G5weY=
|
github.com/antonmedv/expr v1.8.8 h1:uVwIkIBNO2yn4vY2u2DQUqXTmv9jEEMCEcHa19G5weY=
|
||||||
github.com/antonmedv/expr v1.8.8/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
|
github.com/antonmedv/expr v1.8.8/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/bmatcuk/doublestar v1.3.0/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
github.com/bmatcuk/doublestar v1.3.0/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||||
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
|
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
|
||||||
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||||
|
@ -143,6 +146,8 @@ github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB
|
||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
|
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
|
||||||
|
github.com/gorilla/csrf v1.6.2/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
|
||||||
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
@ -235,6 +240,8 @@ github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
|
||||||
|
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
@ -287,6 +294,10 @@ github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShE
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU=
|
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU=
|
||||||
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
|
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
|
||||||
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||||
|
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||||
|
@ -510,6 +521,7 @@ google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
@ -519,6 +531,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
|
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||||
|
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/lithammer/dedent"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
|
@ -19,6 +21,8 @@ type Config struct {
|
||||||
OIDC OIDCConfig `yaml:"oidc"`
|
OIDC OIDCConfig `yaml:"oidc"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
Auth AuthConfig `yaml:"auth"`
|
Auth AuthConfig `yaml:"auth"`
|
||||||
|
SMTP SMTPConfig `yaml:"smtp"`
|
||||||
|
Task TaskConfig `yaml:"task"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFromFile retrieves the configuration from the given file
|
// NewFromFile retrieves the configuration from the given file
|
||||||
|
@ -74,6 +78,29 @@ type AuthConfig struct {
|
||||||
Rules []string `yaml:"rules" env:"AUTH_RULES"`
|
Rules []string `yaml:"rules" env:"AUTH_RULES"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SMTPConfig struct {
|
||||||
|
Host string `yaml:"host" env:"SMTP_HOST"`
|
||||||
|
Port int `yaml:"port" env:"SMTP_PORT"`
|
||||||
|
UseStartTLS bool `yaml:"useStartTLS" env:"SMTP_USE_START_TLS"`
|
||||||
|
User string `yaml:"user" env:"SMTP_USER"`
|
||||||
|
Password string `yaml:"password" env:"SMTP_PASSWORD"`
|
||||||
|
InsecureSkipVerify bool `yaml:"insecureSkipVerify" env:"SMTP_INSECURE_SKIP_VERIFY"`
|
||||||
|
SenderAddress string `yaml:"senderAddress" env:"SMTP_SENDER_ADDRESS"`
|
||||||
|
SenderName string `yaml:"senderName" env:"SMTP_SENDER_NAME"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskConfig struct {
|
||||||
|
Newsletter NewsletterTaskConfig `yaml:"newsletter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewsletterTaskConfig struct {
|
||||||
|
CronSpec string `yaml:"cronSpec" env:"TASK_NEWSLETTER_CRON_SPEC"`
|
||||||
|
TimeRange time.Duration `yaml:"timeRange" env:"TASK_NEWSLETTER_TIME_RANGE"`
|
||||||
|
BaseURL string `yaml:"baseUrl" env:"TASK_NEWSLETTER_BASE_URL"`
|
||||||
|
ContentTemplate string `yaml:"contentTemplate" env:"TASK_NEWSLETTER_CONTENT_TEMPLATE"`
|
||||||
|
SubjectTemplate string `yaml:"subjectTemplate" env:"TASK_NEWSLETTER_SUBJECT_TEMPLATE"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewDumpDefault() *Config {
|
func NewDumpDefault() *Config {
|
||||||
config := NewDefault()
|
config := NewDefault()
|
||||||
return config
|
return config
|
||||||
|
@ -112,6 +139,66 @@ func NewDefault() *Config {
|
||||||
"user.Email endsWith 'cadoles.com'",
|
"user.Email endsWith 'cadoles.com'",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SMTP: SMTPConfig{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 2525,
|
||||||
|
User: "",
|
||||||
|
Password: "",
|
||||||
|
SenderAddress: "noreply@localhost",
|
||||||
|
SenderName: "noreply",
|
||||||
|
},
|
||||||
|
Task: TaskConfig{
|
||||||
|
Newsletter: NewsletterTaskConfig{
|
||||||
|
CronSpec: "0 9 * * 1",
|
||||||
|
TimeRange: 24 * 7 * time.Hour,
|
||||||
|
BaseURL: "http://localhost:8080",
|
||||||
|
ContentTemplate: dedent.Dedent(`
|
||||||
|
{{- $root := . -}}
|
||||||
|
Bonjour {{ .User.Name }},
|
||||||
|
|
||||||
|
{{ if not .HasEvents -}}
|
||||||
|
Aucun évènement notoire ces derniers jours.
|
||||||
|
{{ else -}}
|
||||||
|
Voici les évènements de ces derniers jours:
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{- with .ReadyToVote }}
|
||||||
|
|
||||||
|
Dossiers récemment prêts à voter
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
{{range . -}}
|
||||||
|
- "{{ .Title }}" - {{ $root.BaseURL }}/decisions/{{ .ID }} - créé le {{ .CreatedAt.Format "02/01/2006" }}
|
||||||
|
{{ end }}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{- with .NewDecisionSupportFiles }}
|
||||||
|
|
||||||
|
Nouveaux dossiers d'aide à la décision
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
{{range . -}}
|
||||||
|
- "{{ .Title }}" - {{ $root.BaseURL }}/decisions/{{ .ID }} - créé le {{ .CreatedAt.Format "02/01/2006" }}
|
||||||
|
{{ end }}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{- with .NewWorkgroups}}
|
||||||
|
|
||||||
|
Nouveaux groupes de travail
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
{{range . -}}
|
||||||
|
- "{{ .Name }}" - {{ $root.BaseURL }}/workgroups/{{ .ID }} - créé le {{ .CreatedAt.Format "02/01/2006" }}
|
||||||
|
{{ end }}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
Bonne semaine,
|
||||||
|
|
||||||
|
Daddy
|
||||||
|
`),
|
||||||
|
SubjectTemplate: `[Daddy] Évènements du {{ .From.Format "02/01/2006" }} au {{ .To.Format "02/01/2006" }}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContentTypeHTML = "text/html"
|
||||||
|
ContentTypeText = "text/plain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
InsecureSkipVerify bool
|
||||||
|
UseStartTLS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(*Option)
|
||||||
|
|
||||||
|
type Mailer struct {
|
||||||
|
opt *Option
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTLS(useStartTLS, insecureSkipVerify bool) OptionFunc {
|
||||||
|
return func(opt *Option) {
|
||||||
|
opt.UseStartTLS = useStartTLS
|
||||||
|
opt.InsecureSkipVerify = insecureSkipVerify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithServer(host string, port int) OptionFunc {
|
||||||
|
return func(opt *Option) {
|
||||||
|
opt.Host = host
|
||||||
|
opt.Port = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCredentials(user, password string) OptionFunc {
|
||||||
|
return func(opt *Option) {
|
||||||
|
opt.User = user
|
||||||
|
opt.Password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMailer(funcs ...OptionFunc) *Mailer {
|
||||||
|
opt := &Option{}
|
||||||
|
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Mailer{opt}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import "gitlab.com/wpetit/goweb/service"
|
||||||
|
|
||||||
|
func ServiceProvider(opts ...OptionFunc) service.Provider {
|
||||||
|
mailer := NewMailer(opts...)
|
||||||
|
|
||||||
|
return func(ctn *service.Container) (interface{}, error) {
|
||||||
|
return mailer, nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/mail"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
gomail "gopkg.in/mail.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnexpectedEmailAddressFormat = errors.New("unexpected email address format")
|
||||||
|
)
|
||||||
|
|
||||||
|
type SendFunc func(*SendOption)
|
||||||
|
|
||||||
|
type SendOption struct {
|
||||||
|
Charset string
|
||||||
|
AddressHeaders []AddressHeader
|
||||||
|
Headers []Header
|
||||||
|
Body Body
|
||||||
|
AlternativeBodies []Body
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddressHeader struct {
|
||||||
|
Field string
|
||||||
|
Address string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Header struct {
|
||||||
|
Field string
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Body struct {
|
||||||
|
Type string
|
||||||
|
Content string
|
||||||
|
PartSetting gomail.PartSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCharset(charset string) func(*SendOption) {
|
||||||
|
return func(opt *SendOption) {
|
||||||
|
opt.Charset = charset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSender(address string, name string) func(*SendOption) {
|
||||||
|
return WithAddressHeader("From", address, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSubject(subject string) func(*SendOption) {
|
||||||
|
return WithHeader("Subject", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAddressHeader(field, address, name string) func(*SendOption) {
|
||||||
|
return func(opt *SendOption) {
|
||||||
|
opt.AddressHeaders = append(opt.AddressHeaders, AddressHeader{field, address, name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHeader(field string, values ...string) func(*SendOption) {
|
||||||
|
return func(opt *SendOption) {
|
||||||
|
opt.Headers = append(opt.Headers, Header{field, values})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRecipients(addresses ...string) func(*SendOption) {
|
||||||
|
return WithHeader("To", addresses...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCopies(addresses ...string) func(*SendOption) {
|
||||||
|
return WithHeader("Cc", addresses...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithInvisibleCopies(addresses ...string) func(*SendOption) {
|
||||||
|
return WithHeader("Cci", addresses...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
||||||
|
return func(opt *SendOption) {
|
||||||
|
if setting == nil {
|
||||||
|
setting = gomail.SetPartEncoding(gomail.Unencoded)
|
||||||
|
}
|
||||||
|
opt.Body = Body{contentType, content, setting}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAlternativeBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
||||||
|
return func(opt *SendOption) {
|
||||||
|
if setting == nil {
|
||||||
|
setting = gomail.SetPartEncoding(gomail.Unencoded)
|
||||||
|
}
|
||||||
|
opt.AlternativeBodies = append(opt.AlternativeBodies, Body{contentType, content, setting})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mailer) Send(funcs ...SendFunc) error {
|
||||||
|
opt := &SendOption{
|
||||||
|
Charset: "UTF-8",
|
||||||
|
Body: Body{
|
||||||
|
Type: "text/plain",
|
||||||
|
Content: "",
|
||||||
|
PartSetting: gomail.SetPartEncoding(gomail.Unencoded),
|
||||||
|
},
|
||||||
|
AddressHeaders: make([]AddressHeader, 0),
|
||||||
|
Headers: make([]Header, 0),
|
||||||
|
AlternativeBodies: make([]Body, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range funcs {
|
||||||
|
f(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := m.openConnection()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not open connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
message := gomail.NewMessage(gomail.SetCharset(opt.Charset))
|
||||||
|
|
||||||
|
for _, h := range opt.AddressHeaders {
|
||||||
|
message.SetAddressHeader(h.Field, h.Address, h.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range opt.Headers {
|
||||||
|
message.SetHeader(h.Field, h.Values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
froms := message.GetHeader("From")
|
||||||
|
|
||||||
|
var sendDomain string
|
||||||
|
|
||||||
|
if len(froms) > 0 {
|
||||||
|
sendDomain, err = extractEmailDomain(froms[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messageID := generateMessageID(sendDomain)
|
||||||
|
message.SetHeader("Message-Id", messageID)
|
||||||
|
|
||||||
|
message.SetBody(opt.Body.Type, opt.Body.Content, opt.Body.PartSetting)
|
||||||
|
|
||||||
|
for _, b := range opt.AlternativeBodies {
|
||||||
|
message.AddAlternative(b.Type, b.Content, b.PartSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gomail.Send(conn, message); err != nil {
|
||||||
|
return errors.Wrap(err, "could not send message")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mailer) openConnection() (gomail.SendCloser, error) {
|
||||||
|
dialer := gomail.NewDialer(
|
||||||
|
m.opt.Host,
|
||||||
|
m.opt.Port,
|
||||||
|
m.opt.User,
|
||||||
|
m.opt.Password,
|
||||||
|
)
|
||||||
|
|
||||||
|
if m.opt.InsecureSkipVerify {
|
||||||
|
dialer.TLSConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := dialer.Dial()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not dial smtp server")
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEmailDomain(email string) (string, error) {
|
||||||
|
address, err := mail.ParseAddress(email)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "could not parse email address '%s'", email)
|
||||||
|
}
|
||||||
|
|
||||||
|
addressParts := strings.SplitN(address.Address, "@", 2)
|
||||||
|
if len(addressParts) != 2 { // nolint: gomnd
|
||||||
|
return "", errors.WithStack(ErrUnexpectedEmailAddressFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := addressParts[1]
|
||||||
|
|
||||||
|
return domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateMessageID(domain string) string {
|
||||||
|
// Based on https://www.jwz.org/doc/mid.html
|
||||||
|
timestamp := strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||||
|
random := strconv.FormatInt(rand.Int63(), 36)
|
||||||
|
return fmt.Sprintf("<%s.%s@%s>", timestamp, random, domain)
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ServiceName service.Name = "mail"
|
||||||
|
|
||||||
|
// From retrieves the mail service in the given container
|
||||||
|
func From(container *service.Container) (*Mailer, error) {
|
||||||
|
service, err := container.Service(ServiceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv, ok := service.(*Mailer)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must retrieves the mail service in the given container or panic otherwise
|
||||||
|
func Must(container *service.Container) *Mailer {
|
||||||
|
srv, err := From(container)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv
|
||||||
|
}
|
|
@ -11,6 +11,13 @@ import (
|
||||||
|
|
||||||
const ObjectTypeDecisionSupportFile = "dsf"
|
const ObjectTypeDecisionSupportFile = "dsf"
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusDraft = "draft"
|
||||||
|
StatusReady = "ready"
|
||||||
|
StatusVoted = "voted"
|
||||||
|
StatusClosed = "closed"
|
||||||
|
)
|
||||||
|
|
||||||
type DecisionSupportFile struct {
|
type DecisionSupportFile struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
|
|
@ -80,6 +80,17 @@ func (r *UserRepository) Find(ctx context.Context, id string) (*User, error) {
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) All(ctx context.Context) ([]*User, error) {
|
||||||
|
users := make([]*User, 0)
|
||||||
|
query := r.db.Model(&User{})
|
||||||
|
|
||||||
|
if err := query.Find(&users).Error; err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||||
return &UserRepository{db}
|
return &UserRepository{db}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/mail"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||||
|
|
||||||
|
"gitlab.com/wpetit/goweb/middleware/container"
|
||||||
|
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Newsletter struct {
|
||||||
|
ctx context.Context
|
||||||
|
timeRange time.Duration
|
||||||
|
baseURL string
|
||||||
|
contentTemplate string
|
||||||
|
subjectTemplate string
|
||||||
|
from string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Newsletter) Name() string {
|
||||||
|
return "newsletter"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Newsletter) Run() {
|
||||||
|
ctx := t.ctx
|
||||||
|
|
||||||
|
logger.Info(ctx, "preparing newsletter", logger.F("timeRange", t.timeRange))
|
||||||
|
|
||||||
|
contentTmpl, err := template.New("").Parse(t.contentTemplate)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not parse newsletter content template", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subjectTmpl, err := template.New("").Parse(t.subjectTemplate)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not parse newsletter subject template", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctn := container.Must(ctx)
|
||||||
|
orm := orm.Must(ctn)
|
||||||
|
db := orm.DB()
|
||||||
|
mailSrv := mail.Must(ctn)
|
||||||
|
|
||||||
|
eventRepo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
to := time.Now()
|
||||||
|
from := to.Add(-t.timeRange)
|
||||||
|
|
||||||
|
events, err := eventRepo.Search(ctx, &model.EventFilter{
|
||||||
|
From: &from,
|
||||||
|
To: &to,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not search events", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newWorkgroups := make([]*model.Workgroup, 0)
|
||||||
|
newDecisionSupportFiles := make([]*model.DecisionSupportFile, 0)
|
||||||
|
readyToVote := make([]*model.DecisionSupportFile, 0)
|
||||||
|
|
||||||
|
workgroupRepo := model.NewWorkgroupRepository(db)
|
||||||
|
dsfRepo := model.NewDSFRepository(db)
|
||||||
|
|
||||||
|
for _, evt := range events {
|
||||||
|
switch {
|
||||||
|
case evt.Type == model.EventTypeCreated && evt.ObjectType == model.ObjectTypeWorkgroup:
|
||||||
|
workgroup, err := workgroupRepo.Find(ctx, fmt.Sprintf("%d", evt.ObjectID))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not find workgroup",
|
||||||
|
logger.E(errors.WithStack(err)),
|
||||||
|
logger.F("id", evt.ObjectID),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newWorkgroups = append(newWorkgroups, workgroup)
|
||||||
|
|
||||||
|
case evt.Type == model.EventTypeCreated && evt.ObjectType == model.ObjectTypeDecisionSupportFile:
|
||||||
|
dsf, err := dsfRepo.Find(ctx, fmt.Sprintf("%d", evt.ObjectID))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not find decision support file",
|
||||||
|
logger.E(errors.WithStack(err)),
|
||||||
|
logger.F("id", evt.ObjectID),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newDecisionSupportFiles = append(newDecisionSupportFiles, dsf)
|
||||||
|
|
||||||
|
case evt.Type == model.EventTypeStatusChanged && evt.ObjectType == model.ObjectTypeDecisionSupportFile:
|
||||||
|
dsf, err := dsfRepo.Find(ctx, fmt.Sprintf("%d", evt.ObjectID))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not find decision support file",
|
||||||
|
logger.E(errors.WithStack(err)),
|
||||||
|
logger.F("id", evt.ObjectID),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if dsf.Status == model.StatusReady {
|
||||||
|
readyToVote = append(readyToVote, dsf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasEvents := len(newDecisionSupportFiles) > 0 || len(newWorkgroups) > 0
|
||||||
|
|
||||||
|
userRepo := model.NewUserRepository(db)
|
||||||
|
|
||||||
|
users, err := userRepo.All(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not find users", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
contentBuff bytes.Buffer
|
||||||
|
subjectBuff bytes.Buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, u := range users {
|
||||||
|
tmplData := struct {
|
||||||
|
User *model.User
|
||||||
|
NewWorkgroups []*model.Workgroup
|
||||||
|
NewDecisionSupportFiles []*model.DecisionSupportFile
|
||||||
|
ReadyToVote []*model.DecisionSupportFile
|
||||||
|
BaseURL string
|
||||||
|
From time.Time
|
||||||
|
To time.Time
|
||||||
|
HasEvents bool
|
||||||
|
}{
|
||||||
|
User: u,
|
||||||
|
BaseURL: t.baseURL,
|
||||||
|
NewWorkgroups: newWorkgroups,
|
||||||
|
NewDecisionSupportFiles: newDecisionSupportFiles,
|
||||||
|
ReadyToVote: readyToVote,
|
||||||
|
From: from.Local(),
|
||||||
|
To: to.Local(),
|
||||||
|
HasEvents: hasEvents,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = contentTmpl.Execute(&contentBuff, tmplData)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not execute newsletter content template", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = subjectTmpl.Execute(&subjectBuff, tmplData)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not execute newsletter subject template", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newsletterContent := contentBuff.String()
|
||||||
|
newsletterSubject := subjectBuff.String()
|
||||||
|
|
||||||
|
err := mailSrv.Send(
|
||||||
|
mail.WithRecipients(u.Email),
|
||||||
|
mail.WithSubject(newsletterSubject),
|
||||||
|
mail.WithSender(t.from, ""),
|
||||||
|
mail.WithBody(mail.ContentTypeText, newsletterContent, nil),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not send newsletter",
|
||||||
|
logger.E(errors.WithStack(err)),
|
||||||
|
logger.F("email", u.Email),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBuff.Reset()
|
||||||
|
subjectBuff.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNewsletter(ctx context.Context, timeRange time.Duration, baseURL, contentTemplate, subjectTemplate, from string) *Newsletter {
|
||||||
|
return &Newsletter{
|
||||||
|
ctx: ctx,
|
||||||
|
timeRange: timeRange,
|
||||||
|
baseURL: baseURL,
|
||||||
|
contentTemplate: contentTemplate,
|
||||||
|
subjectTemplate: subjectTemplate,
|
||||||
|
from: from,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package task
|
||||||
|
|
||||||
|
type Task interface {
|
||||||
|
Name() string
|
||||||
|
Run()
|
||||||
|
}
|
Loading…
Reference in New Issue