mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-02-15 20:33:26 +00:00
Allow notification templates to fail
Templates were hardcoded. To make them dynamic, errors must be handled.
This commit is contained in:
parent
42d631876d
commit
dd9937740a
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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}"))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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}}""")
|
||||
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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}}""")
|
||||
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user