diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/EventContext.scala b/modules/notification/api/src/main/scala/docspell/notification/api/EventContext.scala index 075d25eb..fc3f2984 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/EventContext.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/EventContext.scala @@ -31,30 +31,33 @@ trait EventContext { "content" -> content ) - def defaultTitle: String - def defaultTitleHtml: String + def defaultTitle: Either[String, String] + def defaultTitleHtml: Either[String, String] - def defaultBody: String - def defaultBodyHtml: String + def defaultBody: Either[String, String] + def defaultBodyHtml: Either[String, String] - def defaultBoth: String - def defaultBothHtml: String + def defaultBoth: Either[String, String] + def defaultBothHtml: Either[String, String] - lazy val asJsonWithMessage: Json = { - val data = asJson - val msg = Json.obj( - "message" -> Json.obj( - "title" -> defaultTitle.asJson, - "body" -> defaultBody.asJson - ), - "messageHtml" -> Json.obj( - "title" -> defaultTitleHtml.asJson, - "body" -> defaultBodyHtml.asJson + lazy val asJsonWithMessage: Either[String, Json] = + for { + tt1 <- defaultTitle + tb1 <- defaultBody + tt2 <- defaultTitleHtml + tb2 <- defaultBodyHtml + data = asJson + msg = Json.obj( + "message" -> Json.obj( + "title" -> tt1.asJson, + "body" -> tb1.asJson + ), + "messageHtml" -> Json.obj( + "title" -> tt2.asJson, + "body" -> tb2.asJson + ) ) - ) - - data.withObject(o1 => msg.withObject(o2 => o1.deepMerge(o2).asJson)) - } + } yield data.withObject(o1 => msg.withObject(o2 => o1.deepMerge(o2).asJson)) } object EventContext { @@ -62,12 +65,12 @@ object EventContext { new EventContext { val event = ev def content = Json.obj() - def defaultTitle = "" - def defaultTitleHtml = "" - def defaultBody = "" - def defaultBodyHtml = "" - def defaultBoth: String = "" - def defaultBothHtml: String = "" + def defaultTitle = Right("") + def defaultTitleHtml = Right("") + def defaultBody = Right("") + def defaultBodyHtml = Right("") + def defaultBoth = Right("") + def defaultBothHtml = Right("") } /** For an event, the context can be created that is usually amended with more diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/AbstractEventContext.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/AbstractEventContext.scala index 0d42e43c..04dc3990 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/AbstractEventContext.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/AbstractEventContext.scala @@ -14,9 +14,9 @@ import yamusca.imports._ abstract class AbstractEventContext extends EventContext { - def titleTemplate: Template + def titleTemplate: Either[String, Template] - def bodyTemplate: Template + def bodyTemplate: Either[String, Template] def render(template: Template): String = asJson.render(template).trim() @@ -24,33 +24,39 @@ abstract class AbstractEventContext extends EventContext { def renderHtml(template: Template): String = Markdown.toHtml(render(template)) - lazy val defaultTitle: String = - render(titleTemplate) + lazy val defaultTitle: Either[String, String] = + titleTemplate.map(render) - lazy val defaultTitleHtml: String = - renderHtml(titleTemplate) + lazy val defaultTitleHtml: Either[String, String] = + titleTemplate.map(renderHtml) - lazy val defaultBody: String = - render(bodyTemplate) + lazy val defaultBody: Either[String, String] = + bodyTemplate.map(render) - lazy val defaultBodyHtml: String = - renderHtml(bodyTemplate) + lazy val defaultBodyHtml: Either[String, String] = + bodyTemplate.map(renderHtml) - lazy val defaultBoth: String = - render( + lazy val defaultBoth: Either[String, String] = + for { + tt <- titleTemplate + tb <- bodyTemplate + } yield render( AbstractEventContext.concat( - titleTemplate, + tt, AbstractEventContext.sepTemplate, - bodyTemplate + tb ) ) - lazy val defaultBothHtml: String = - renderHtml( + lazy val defaultBothHtml: Either[String, String] = + for { + tt <- titleTemplate + tb <- bodyTemplate + } yield renderHtml( AbstractEventContext.concat( - titleTemplate, + tt, AbstractEventContext.sepTemplate, - bodyTemplate + tb ) ) } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/EmailBackend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/EmailBackend.scala index f93202c8..81a016c2 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/EmailBackend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/EmailBackend.scala @@ -19,22 +19,24 @@ final class EmailBackend[F[_]: Sync]( channel: NotificationChannel.Email, mailService: Emil[F], logger: Logger[F] -) extends NotificationBackend[F] { +) extends NotificationBackend[F] + with EventContextSyntax { import emil.builder._ - def send(event: EventContext): F[Unit] = { - val mail = - MailBuilder.build( - From(channel.from), - Tos(channel.recipients.toList), - Subject(event.defaultTitle), - MarkdownBody[F](event.defaultBody) - ) + def send(event: EventContext): F[Unit] = + event.withDefault(logger) { (title, body) => + val mail = + MailBuilder.build( + From(channel.from), + Tos(channel.recipients.toList), + Subject(title), + MarkdownBody[F](body) + ) - logger.debug(s"Attempting to send notification mail: $channel") *> - mailService(channel.config) - .send(mail) - .flatMap(msgId => logger.info(s"Send notification mail ${msgId.head}")) - } + logger.debug(s"Attempting to send notification mail: $channel") *> + mailService(channel.config) + .send(mail) + .flatMap(msgId => logger.info(s"Send notification mail ${msgId.head}")) + } } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/EventContextSyntax.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/EventContextSyntax.scala new file mode 100644 index 00000000..781fdbb5 --- /dev/null +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/EventContextSyntax.scala @@ -0,0 +1,30 @@ +package docspell.notification.impl + +import docspell.notification.api.EventContext +import docspell.common.Logger +import io.circe.Json + +trait EventContextSyntax { + private def logError[F[_]](logger: Logger[F])(reason: String): F[Unit] = + logger.error(s"Unable to send notification, the template failed to render: $reason") + + implicit final class EventContextOps(self: EventContext) { + def withDefault[F[_]](logger: Logger[F])(f: (String, String) => F[Unit]): F[Unit] = + (for { + tt <- self.defaultTitle + tb <- self.defaultBody + } yield f(tt, tb)).fold(logError(logger), identity) + + def withJsonMessage[F[_]](logger: Logger[F])(f: Json => F[Unit]): F[Unit] = + self.asJsonWithMessage match { + case Right(m) => f(m) + case Left(err) => logError(logger)(err) + } + + def withDefaultBoth[F[_]](logger: Logger[F])(f: (String, String) => F[Unit]): F[Unit] = + (for { + md <- self.defaultBoth + html <- self.defaultBothHtml + } yield f(md, html)).fold(logError(logger), identity) + } +} diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/GotifyBackend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/GotifyBackend.scala index 2d955a8a..02415726 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/GotifyBackend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/GotifyBackend.scala @@ -23,27 +23,29 @@ final class GotifyBackend[F[_]: Async]( channel: NotificationChannel.Gotify, client: Client[F], logger: Logger[F] -) extends NotificationBackend[F] { +) extends NotificationBackend[F] + with EventContextSyntax { val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {} import dsl._ - def send(event: EventContext): F[Unit] = { - val url = Uri.unsafeFromString((channel.url / "message").asString) - val req = POST( - Json.obj( - "title" -> Json.fromString(event.defaultTitle), - "message" -> Json.fromString(event.defaultBody), - "extras" -> Json.obj( - "client::display" -> Json.obj( - "contentType" -> Json.fromString("text/markdown") + def send(event: EventContext): F[Unit] = + event.withDefault(logger) { (title, body) => + val url = Uri.unsafeFromString((channel.url / "message").asString) + val req = POST( + Json.obj( + "title" -> Json.fromString(title), + "message" -> Json.fromString(body), + "extras" -> Json.obj( + "client::display" -> Json.obj( + "contentType" -> Json.fromString("text/markdown") + ) ) - ) - ), - url - ) - .putHeaders("X-Gotify-Key" -> channel.appKey.pass) - logger.debug(s"Seding request: $req") *> - HttpSend.sendRequest(client, req, channel, logger) - } + ), + url + ) + .putHeaders("X-Gotify-Key" -> channel.appKey.pass) + logger.debug(s"Seding request: $req") *> + HttpSend.sendRequest(client, req, channel, logger) + } } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpPostBackend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpPostBackend.scala index 226f5522..0a5acb21 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpPostBackend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/HttpPostBackend.scala @@ -21,16 +21,18 @@ final class HttpPostBackend[F[_]: Async]( channel: NotificationChannel.HttpPost, client: Client[F], logger: Logger[F] -) extends NotificationBackend[F] { +) extends NotificationBackend[F] + with EventContextSyntax { val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {} import dsl._ import org.http4s.circe.CirceEntityCodec._ - def send(event: EventContext): F[Unit] = { - val url = Uri.unsafeFromString(channel.url.asString) - val req = POST(event.asJsonWithMessage, url).putHeaders(channel.headers.toList) - logger.debug(s"$channel sending request: $req") *> - HttpSend.sendRequest(client, req, channel, logger) - } + def send(event: EventContext): F[Unit] = + event.withJsonMessage(logger) { json => + val url = Uri.unsafeFromString(channel.url.asString) + val req = POST(json, url).putHeaders(channel.headers.toList) + logger.debug(s"$channel sending request: $req") *> + HttpSend.sendRequest(client, req, channel, logger) + } } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/MatrixBackend.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/MatrixBackend.scala index 4222b004..41f0cb9b 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/MatrixBackend.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/MatrixBackend.scala @@ -20,26 +20,28 @@ final class MatrixBackend[F[_]: Async]( channel: NotificationChannel.Matrix, client: Client[F], logger: Logger[F] -) extends NotificationBackend[F] { +) extends NotificationBackend[F] + with EventContextSyntax { val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {} import dsl._ import org.http4s.circe.CirceEntityCodec._ - def send(event: EventContext): F[Unit] = { - val url = - (channel.homeServer / "_matrix" / "client" / "r0" / "rooms" / channel.roomId / "send" / "m.room.message") - .withQuery("access_token", channel.accessToken.pass) - val uri = Uri.unsafeFromString(url.asString) - val req = POST( - Map( - "msgtype" -> channel.messageType, - "format" -> "org.matrix.custom.html", - "formatted_body" -> event.defaultBothHtml, - "body" -> event.defaultBoth - ), - uri - ) - HttpSend.sendRequest(client, req, channel, logger) - } + def send(event: EventContext): F[Unit] = + event.withDefaultBoth(logger) { (md, html) => + val url = + (channel.homeServer / "_matrix" / "client" / "r0" / "rooms" / channel.roomId / "send" / "m.room.message") + .withQuery("access_token", channel.accessToken.pass) + val uri = Uri.unsafeFromString(url.asString) + val req = POST( + Map( + "msgtype" -> channel.messageType, + "format" -> "org.matrix.custom.html", + "formatted_body" -> html, + "body" -> md + ), + uri + ) + HttpSend.sendRequest(client, req, channel, logger) + } } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/DeleteFieldValueCtx.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/DeleteFieldValueCtx.scala index cbee4f07..1a96071a 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/DeleteFieldValueCtx.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/DeleteFieldValueCtx.scala @@ -30,9 +30,9 @@ final case class DeleteFieldValueCtx( val content = data.asJson - val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)" + val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)") val bodyTemplate = - mustache"""{{#content}}{{#field.label}}*{{field.label}}* {{/field.label}}{{^field.label}}*{{field.name}}* {{/field.label}} was removed from {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""" + Right(mustache"""{{#content}}{{#field.label}}*{{field.label}}* {{/field.label}}{{^field.label}}*{{field.name}}* {{/field.label}} was removed from {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""") } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/ItemSelectionCtx.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/ItemSelectionCtx.scala index 9c3a700d..2fd19eb6 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/ItemSelectionCtx.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/ItemSelectionCtx.scala @@ -25,8 +25,8 @@ final case class ItemSelectionCtx(event: Event.ItemSelection, data: ItemSelectio val content = data.asJson - val titleTemplate = mustache"Your items" - val bodyTemplate = mustache""" + val titleTemplate = Right(mustache"Your items") + val bodyTemplate = Right(mustache""" Hello {{{ content.username }}}, this is Docspell informing you about your next items. @@ -51,7 +51,7 @@ this is Docspell informing you about your next items. Sincerely yours, Docspell -""" +""") } object ItemSelectionCtx { @@ -113,5 +113,4 @@ object ItemSelectionCtx { account.user.id ) } - } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/JobDoneCtx.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/JobDoneCtx.scala index 48009186..43e21d3b 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/JobDoneCtx.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/JobDoneCtx.scala @@ -22,8 +22,8 @@ final case class JobDoneCtx(event: Event.JobDone, data: JobDoneCtx.Data) val content = data.asJson - val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)" - val bodyTemplate = mustache"""{{#content}}_'{{subject}}'_ finished {{/content}}""" + val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)") + val bodyTemplate = Right(mustache"""{{#content}}_'{{subject}}'_ finished {{/content}}""") } object JobDoneCtx { diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/JobSubmittedCtx.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/JobSubmittedCtx.scala index 3fb0bf62..045fd7b3 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/JobSubmittedCtx.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/JobSubmittedCtx.scala @@ -22,9 +22,9 @@ final case class JobSubmittedCtx(event: Event.JobSubmitted, data: JobSubmittedCt val content = data.asJson - val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)" + val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)") val bodyTemplate = - mustache"""{{#content}}_'{{subject}}'_ submitted by {{submitter}} {{/content}}""" + Right(mustache"""{{#content}}_'{{subject}}'_ submitted by {{submitter}} {{/content}}""") } object JobSubmittedCtx { diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/SetFieldValueCtx.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/SetFieldValueCtx.scala index fd67c714..60feb607 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/SetFieldValueCtx.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/SetFieldValueCtx.scala @@ -28,9 +28,9 @@ final case class SetFieldValueCtx(event: Event.SetFieldValue, data: SetFieldValu val content = data.asJson - val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)" + val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)") val bodyTemplate = - mustache"""{{#content}}{{#field.label}}*{{field.label}}* {{/field.label}}{{^field.label}}*{{field.name}}* {{/field.label}} was set to '{{value}}' on {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""" + Right(mustache"""{{#content}}{{#field.label}}*{{field.label}}* {{/field.label}}{{^field.label}}*{{field.name}}* {{/field.label}} was set to '{{value}}' on {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""") } diff --git a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/TagsChangedCtx.scala b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/TagsChangedCtx.scala index ee536482..a6410b50 100644 --- a/modules/notification/impl/src/main/scala/docspell/notification/impl/context/TagsChangedCtx.scala +++ b/modules/notification/impl/src/main/scala/docspell/notification/impl/context/TagsChangedCtx.scala @@ -26,10 +26,9 @@ final case class TagsChangedCtx(event: Event.TagsChanged, data: TagsChangedCtx.D val content = data.asJson - val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)" + val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)") val bodyTemplate = - mustache"""{{#content}}{{#added}}{{#-first}}Adding {{/-first}}{{^-first}}, {{/-first}}*{{name}}*{{/added}}{{#removed}}{{#added}}{{#-first}};{{/-first}}{{/added}}{{#-first}} Removing {{/-first}}{{^-first}}, {{/-first}}*{{name}}*{{/removed}} on {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""" - + Right(mustache"""{{#content}}{{#added}}{{#-first}}Adding {{/-first}}{{^-first}}, {{/-first}}*{{name}}*{{/added}}{{#removed}}{{#added}}{{#-first}};{{/-first}}{{/added}}{{#-first}} Removing {{/-first}}{{^-first}}, {{/-first}}*{{name}}*{{/removed}} on {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""") } object TagsChangedCtx { diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala index c462009e..22d6ec87 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala @@ -162,7 +162,11 @@ object NotificationRoutes { user.account, baseUrl.some ) - resp <- Ok(data.asJsonWithMessage) + resp <- data.asJsonWithMessage match { + case Right(m) => Ok(m) + case Left(err) => + BadRequest(BasicResult(false, s"Unable to render message: $err")) + } } yield resp } }