From 58719182c41df39470425f0503378dfe4de3793c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Fri, 6 Aug 2021 23:28:03 +0200 Subject: [PATCH] chore: notify users about failing wakatime connection --- config/eventbus.go | 1 + go.mod | 1 - go.sum | 19 +-- main.go | 16 +- middlewares/custom/wakatime.go | 23 ++- services/mail/mail.go | 43 ++++- services/mail/types.go | 5 + services/services.go | 1 + services/user.go | 47 ++++-- .../mail/wakatime_connection_failure.tpl.html | 152 ++++++++++++++++++ 10 files changed, 268 insertions(+), 40 deletions(-) create mode 100644 views/mail/wakatime_connection_failure.tpl.html diff --git a/config/eventbus.go b/config/eventbus.go index 570dc3c..f349604 100644 --- a/config/eventbus.go +++ b/config/eventbus.go @@ -15,6 +15,7 @@ const ( EventHeartbeatCreate = "heartbeat.create" EventProjectLabelCreate = "project_label.create" EventProjectLabelDelete = "project_label.delete" + EventWakatimeFailure = "wakatime.failure" FieldPayload = "payload" FieldUser = "user" FieldUserId = "user.id" diff --git a/go.mod b/go.mod index 7afddff..d51bf05 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,6 @@ require ( go.uber.org/atomic v1.9.0 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/text v0.3.6 // indirect golang.org/x/tools v0.1.0 // indirect gorm.io/driver/mysql v1.1.1 gorm.io/driver/postgres v1.1.0 diff --git a/go.sum b/go.sum index eaf41b6..31a092f 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -136,8 +135,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -223,7 +222,6 @@ github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5 github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.8.1 h1:ySBX7Q87vOMqKU2bbmKbUvtYhauDFclYbNDYIE1/h6s= github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= @@ -231,9 +229,9 @@ github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU= github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -245,8 +243,6 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.0 h1:h2yg3kjIyAGSZKDijYn1/gXHlYLCwl9ZjEh2PU0yVxE= -github.com/jackc/pgproto3/v2 v2.1.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= @@ -258,7 +254,6 @@ github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrU github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= -github.com/jackc/pgtype v1.7.0 h1:6f4kVsW01QftE38ufBYxKciO6gyioXSC0ABIRLcZrGs= github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs= @@ -269,7 +264,6 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= -github.com/jackc/pgx/v4 v4.11.0 h1:J86tSWd3Y7nKjwT/43xZBvpi04keQWx8gNC2YkdJhZI= github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570= @@ -323,8 +317,8 @@ github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.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/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= @@ -450,8 +444,8 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -518,8 +512,6 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.8.0 h1:CUhrE4N1rqSE6FM9ecihEjRkLQu8cDfgDyoOs83mEY4= -go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -544,7 +536,6 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -717,8 +708,6 @@ gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= -gorm.io/gorm v1.21.11 h1:CxkXW6Cc+VIBlL8yJEHq+Co4RYXdSLiMKNvgoZPjLK4= -gorm.io/gorm v1.21.11/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.21.12 h1:3fQM0Eiz7jcJEhPggHEpoYnsGZqynMzverL77DV40RM= gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 0f56c9e..6566455 100644 --- a/main.go +++ b/main.go @@ -145,14 +145,14 @@ func main() { keyValueRepository = repositories.NewKeyValueRepository(db) // Services + mailService = mail.NewMailService() aliasService = services.NewAliasService(aliasRepository) - userService = services.NewUserService(userRepository) + userService = services.NewUserService(mailService, userRepository) languageMappingService = services.NewLanguageMappingService(languageMappingRepository) projectLabelService = services.NewProjectLabelService(projectLabelRepository) heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService) summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService, projectLabelService) aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService) - mailService = mail.NewMailService() keyValueService = services.NewKeyValueService(keyValueRepository) reportService = services.NewReportService(summaryService, userService, mailService) miscService = services.NewMiscService(userService, summaryService, keyValueService) @@ -240,11 +240,13 @@ func main() { // Miscellaneous // Pre-warm projects cache - allUsers, err := userService.GetAll() - if err == nil { - logbuch.Info("pre-warming user project cache") - for _, u := range allUsers { - go heartbeatService.GetEntitySetByUser(models.SummaryProject, u) + if !config.IsDev() { + allUsers, err := userService.GetAll() + if err == nil { + logbuch.Info("pre-warming user project cache") + for _, u := range allUsers { + go heartbeatService.GetEntitySetByUser(models.SummaryProject, u) + } } } diff --git a/middlewares/custom/wakatime.go b/middlewares/custom/wakatime.go index 285b9ab..358c959 100644 --- a/middlewares/custom/wakatime.go +++ b/middlewares/custom/wakatime.go @@ -5,18 +5,24 @@ import ( "encoding/base64" "fmt" "github.com/emvi/logbuch" + "github.com/leandro-lugaresi/hub" "github.com/muety/wakapi/config" "github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/models" + "github.com/patrickmn/go-cache" "io" "io/ioutil" "net/http" "time" ) +const maxFailuresPerDay = 100 + /* Middleware to conditionally relay heartbeats to Wakatime */ type WakatimeRelayMiddleware struct { - httpClient *http.Client + httpClient *http.Client + failureCache *cache.Cache + eventBus *hub.Hub } func NewWakatimeRelayMiddleware() *WakatimeRelayMiddleware { @@ -24,6 +30,8 @@ func NewWakatimeRelayMiddleware() *WakatimeRelayMiddleware { httpClient: &http.Client{ Timeout: 10 * time.Second, }, + failureCache: cache.New(24*time.Hour, 1*time.Hour), + eventBus: config.EventBus(), } } @@ -92,5 +100,18 @@ func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, heade if response.StatusCode < 200 || response.StatusCode >= 300 { logbuch.Warn("failed to relay request for user %s, got status %d", forUser.ID, response.StatusCode) + + // TODO: use leaky bucket instead of expiring cache? + if _, found := m.failureCache.Get(forUser.ID); !found { + m.failureCache.SetDefault(forUser.ID, 0) + } + if n, _ := m.failureCache.IncrementInt(forUser.ID, 1); n == maxFailuresPerDay { + m.eventBus.Publish(hub.Message{ + Name: config.EventWakatimeFailure, + Fields: map[string]interface{}{config.FieldUser: forUser, config.FieldPayload: n}, + }) + } else if n%10 == 0 { + logbuch.Warn("%d / %d failed wakatime heartbeat relaying attempts for user %s within last 24 hours", n, maxFailuresPerDay, forUser.ID) + } } } diff --git a/services/mail/mail.go b/services/mail/mail.go index c27e854..1dc3163 100644 --- a/services/mail/mail.go +++ b/services/mail/mail.go @@ -16,12 +16,14 @@ import ( ) const ( - tplNamePasswordReset = "reset_password" - tplNameImportNotification = "import_finished" - tplNameReport = "report" - subjectPasswordReset = "Wakapi - Password Reset" - subjectImportNotification = "Wakapi - Data Import Finished" - subjectReport = "Wakapi - Report from %s" + tplNamePasswordReset = "reset_password" + tplNameImportNotification = "import_finished" + tplNameWakatimeFailureNotification = "wakatime_connection_failure" + tplNameReport = "report" + subjectPasswordReset = "Wakapi - Password Reset" + subjectImportNotification = "Wakapi - Data Import Finished" + subjectWakatimeFailureNotification = "Wakapi - WakaTime Connection Failure" + subjectReport = "Wakapi - Report from %s" ) type SendingService interface { @@ -64,6 +66,23 @@ func (m *MailService) SendPasswordReset(recipient *models.User, resetLink string return m.sendingService.Send(mail) } +func (m *MailService) SendWakatimeFailureNotification(recipient *models.User, numFailures int) error { + tpl, err := getWakatimeFailureNotificationTemplate(WakatimeFailureNotificationNotificationTplData{ + PublicUrl: m.config.Server.PublicUrl, + NumFailures: numFailures, + }) + if err != nil { + return err + } + mail := &models.Mail{ + From: models.MailAddress(m.config.Mail.Sender), + To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}), + Subject: subjectWakatimeFailureNotification, + } + mail.WithHTML(tpl.String()) + return m.sendingService.Send(mail) +} + func (m *MailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error { tpl, err := getImportNotificationTemplate(ImportNotificationTplData{ PublicUrl: m.config.Server.PublicUrl, @@ -108,6 +127,18 @@ func getPasswordResetTemplate(data PasswordResetTplData) (*bytes.Buffer, error) return &rendered, nil } +func getWakatimeFailureNotificationTemplate(data WakatimeFailureNotificationNotificationTplData) (*bytes.Buffer, error) { + tpl, err := loadTemplate(tplNameWakatimeFailureNotification) + if err != nil { + return nil, err + } + var rendered bytes.Buffer + if err := tpl.Execute(&rendered, data); err != nil { + return nil, err + } + return &rendered, nil +} + func getImportNotificationTemplate(data ImportNotificationTplData) (*bytes.Buffer, error) { tpl, err := loadTemplate(tplNameImportNotification) if err != nil { diff --git a/services/mail/types.go b/services/mail/types.go index 5b74b82..3b4b321 100644 --- a/services/mail/types.go +++ b/services/mail/types.go @@ -12,6 +12,11 @@ type ImportNotificationTplData struct { NumHeartbeats int } +type WakatimeFailureNotificationNotificationTplData struct { + PublicUrl string + NumFailures int +} + type ReportTplData struct { Report *models.Report } diff --git a/services/services.go b/services/services.go index 9e37af1..f3fd545 100644 --- a/services/services.go +++ b/services/services.go @@ -64,6 +64,7 @@ type IProjectLabelService interface { type IMailService interface { SendPasswordReset(*models.User, string) error + SendWakatimeFailureNotification(*models.User, int) error SendImportNotification(*models.User, time.Duration, int) error SendReport(*models.User, *models.Report) error } diff --git a/services/user.go b/services/user.go index 7172edd..b145542 100644 --- a/services/user.go +++ b/services/user.go @@ -2,6 +2,7 @@ package services import ( "fmt" + "github.com/emvi/logbuch" "github.com/leandro-lugaresi/hub" "github.com/muety/wakapi/config" "github.com/muety/wakapi/models" @@ -13,19 +14,45 @@ import ( ) type UserService struct { - config *config.Config - cache *cache.Cache - eventBus *hub.Hub - repository repositories.IUserRepository + config *config.Config + cache *cache.Cache + eventBus *hub.Hub + mailService IMailService + repository repositories.IUserRepository } -func NewUserService(userRepo repositories.IUserRepository) *UserService { - return &UserService{ - config: config.Get(), - eventBus: config.EventBus(), - cache: cache.New(1*time.Hour, 2*time.Hour), - repository: userRepo, +func NewUserService(mailService IMailService, userRepo repositories.IUserRepository) *UserService { + srv := &UserService{ + config: config.Get(), + eventBus: config.EventBus(), + cache: cache.New(1*time.Hour, 2*time.Hour), + mailService: mailService, + repository: userRepo, } + + sub1 := srv.eventBus.Subscribe(0, config.EventWakatimeFailure) + go func(sub *hub.Subscription) { + for m := range sub.Receiver { + user := m.Fields[config.FieldUser].(*models.User) + n := m.Fields[config.FieldPayload].(int) + + logbuch.Warn("resetting wakatime api key for user %s, because of too many failures (%d)", user.ID, n) + + if _, err := srv.SetWakatimeApiKey(user, ""); err != nil { + logbuch.Error("failed to set wakatime api key for user %s", user.ID) + } + + if user.Email != "" { + if err := mailService.SendWakatimeFailureNotification(user, n); err != nil { + logbuch.Error("failed to send wakatime failure notification mail to user %s", user.ID) + } else { + logbuch.Info("sent wakatime connection failure mail to %s", user.ID) + } + } + } + }(&sub1) + + return srv } func (srv *UserService) GetUserById(userId string) (*models.User, error) { diff --git a/views/mail/wakatime_connection_failure.tpl.html b/views/mail/wakatime_connection_failure.tpl.html new file mode 100644 index 0000000..2ef4ca4 --- /dev/null +++ b/views/mail/wakatime_connection_failure.tpl.html @@ -0,0 +1,152 @@ + + + + + + Wakapi – WakaTime Connection Failure + + + + + + + + + +
  +
+ + + + +
+ Wakapi Logo +
+
+ +
+ + + + +
+ + + + +
+

WakaTime Connection Failure

+

You have configured Wakapi to relay your heartbeats to WakaTime's API. However, requests for the last {{ .NumFailures }} heartbeats have failed. This is most likely an authentication issue. WakaTime connection is paused for now. To resume it, please re-enter your WakaTime API token under Settings.

+ + + + + + +
+ + + + + + +
Go to Settings
+
+
+
+ + +
+
 
+ +