mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Allow notification templates to fail
Templates were hardcoded. To make them dynamic, errors must be handled.
This commit is contained in:
@ -31,30 +31,33 @@ trait EventContext {
|
|||||||
"content" -> content
|
"content" -> content
|
||||||
)
|
)
|
||||||
|
|
||||||
def defaultTitle: String
|
def defaultTitle: Either[String, String]
|
||||||
def defaultTitleHtml: String
|
def defaultTitleHtml: Either[String, String]
|
||||||
|
|
||||||
def defaultBody: String
|
def defaultBody: Either[String, String]
|
||||||
def defaultBodyHtml: String
|
def defaultBodyHtml: Either[String, String]
|
||||||
|
|
||||||
def defaultBoth: String
|
def defaultBoth: Either[String, String]
|
||||||
def defaultBothHtml: String
|
def defaultBothHtml: Either[String, String]
|
||||||
|
|
||||||
lazy val asJsonWithMessage: Json = {
|
lazy val asJsonWithMessage: Either[String, Json] =
|
||||||
val data = asJson
|
for {
|
||||||
val msg = Json.obj(
|
tt1 <- defaultTitle
|
||||||
|
tb1 <- defaultBody
|
||||||
|
tt2 <- defaultTitleHtml
|
||||||
|
tb2 <- defaultBodyHtml
|
||||||
|
data = asJson
|
||||||
|
msg = Json.obj(
|
||||||
"message" -> Json.obj(
|
"message" -> Json.obj(
|
||||||
"title" -> defaultTitle.asJson,
|
"title" -> tt1.asJson,
|
||||||
"body" -> defaultBody.asJson
|
"body" -> tb1.asJson
|
||||||
),
|
),
|
||||||
"messageHtml" -> Json.obj(
|
"messageHtml" -> Json.obj(
|
||||||
"title" -> defaultTitleHtml.asJson,
|
"title" -> tt2.asJson,
|
||||||
"body" -> defaultBodyHtml.asJson
|
"body" -> tb2.asJson
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} yield data.withObject(o1 => msg.withObject(o2 => o1.deepMerge(o2).asJson))
|
||||||
data.withObject(o1 => msg.withObject(o2 => o1.deepMerge(o2).asJson))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object EventContext {
|
object EventContext {
|
||||||
@ -62,12 +65,12 @@ object EventContext {
|
|||||||
new EventContext {
|
new EventContext {
|
||||||
val event = ev
|
val event = ev
|
||||||
def content = Json.obj()
|
def content = Json.obj()
|
||||||
def defaultTitle = ""
|
def defaultTitle = Right("")
|
||||||
def defaultTitleHtml = ""
|
def defaultTitleHtml = Right("")
|
||||||
def defaultBody = ""
|
def defaultBody = Right("")
|
||||||
def defaultBodyHtml = ""
|
def defaultBodyHtml = Right("")
|
||||||
def defaultBoth: String = ""
|
def defaultBoth = Right("")
|
||||||
def defaultBothHtml: String = ""
|
def defaultBothHtml = Right("")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** For an event, the context can be created that is usually amended with more
|
/** 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 {
|
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 =
|
def render(template: Template): String =
|
||||||
asJson.render(template).trim()
|
asJson.render(template).trim()
|
||||||
@ -24,33 +24,39 @@ abstract class AbstractEventContext extends EventContext {
|
|||||||
def renderHtml(template: Template): String =
|
def renderHtml(template: Template): String =
|
||||||
Markdown.toHtml(render(template))
|
Markdown.toHtml(render(template))
|
||||||
|
|
||||||
lazy val defaultTitle: String =
|
lazy val defaultTitle: Either[String, String] =
|
||||||
render(titleTemplate)
|
titleTemplate.map(render)
|
||||||
|
|
||||||
lazy val defaultTitleHtml: String =
|
lazy val defaultTitleHtml: Either[String, String] =
|
||||||
renderHtml(titleTemplate)
|
titleTemplate.map(renderHtml)
|
||||||
|
|
||||||
lazy val defaultBody: String =
|
lazy val defaultBody: Either[String, String] =
|
||||||
render(bodyTemplate)
|
bodyTemplate.map(render)
|
||||||
|
|
||||||
lazy val defaultBodyHtml: String =
|
lazy val defaultBodyHtml: Either[String, String] =
|
||||||
renderHtml(bodyTemplate)
|
bodyTemplate.map(renderHtml)
|
||||||
|
|
||||||
lazy val defaultBoth: String =
|
lazy val defaultBoth: Either[String, String] =
|
||||||
render(
|
for {
|
||||||
|
tt <- titleTemplate
|
||||||
|
tb <- bodyTemplate
|
||||||
|
} yield render(
|
||||||
AbstractEventContext.concat(
|
AbstractEventContext.concat(
|
||||||
titleTemplate,
|
tt,
|
||||||
AbstractEventContext.sepTemplate,
|
AbstractEventContext.sepTemplate,
|
||||||
bodyTemplate
|
tb
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val defaultBothHtml: String =
|
lazy val defaultBothHtml: Either[String, String] =
|
||||||
renderHtml(
|
for {
|
||||||
|
tt <- titleTemplate
|
||||||
|
tb <- bodyTemplate
|
||||||
|
} yield renderHtml(
|
||||||
AbstractEventContext.concat(
|
AbstractEventContext.concat(
|
||||||
titleTemplate,
|
tt,
|
||||||
AbstractEventContext.sepTemplate,
|
AbstractEventContext.sepTemplate,
|
||||||
bodyTemplate
|
tb
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -19,17 +19,19 @@ final class EmailBackend[F[_]: Sync](
|
|||||||
channel: NotificationChannel.Email,
|
channel: NotificationChannel.Email,
|
||||||
mailService: Emil[F],
|
mailService: Emil[F],
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
) extends NotificationBackend[F] {
|
) extends NotificationBackend[F]
|
||||||
|
with EventContextSyntax {
|
||||||
|
|
||||||
import emil.builder._
|
import emil.builder._
|
||||||
|
|
||||||
def send(event: EventContext): F[Unit] = {
|
def send(event: EventContext): F[Unit] =
|
||||||
|
event.withDefault(logger) { (title, body) =>
|
||||||
val mail =
|
val mail =
|
||||||
MailBuilder.build(
|
MailBuilder.build(
|
||||||
From(channel.from),
|
From(channel.from),
|
||||||
Tos(channel.recipients.toList),
|
Tos(channel.recipients.toList),
|
||||||
Subject(event.defaultTitle),
|
Subject(title),
|
||||||
MarkdownBody[F](event.defaultBody)
|
MarkdownBody[F](body)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(s"Attempting to send notification mail: $channel") *>
|
logger.debug(s"Attempting to send notification mail: $channel") *>
|
||||||
|
@ -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,17 +23,19 @@ final class GotifyBackend[F[_]: Async](
|
|||||||
channel: NotificationChannel.Gotify,
|
channel: NotificationChannel.Gotify,
|
||||||
client: Client[F],
|
client: Client[F],
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
) extends NotificationBackend[F] {
|
) extends NotificationBackend[F]
|
||||||
|
with EventContextSyntax {
|
||||||
|
|
||||||
val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {}
|
val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
def send(event: EventContext): F[Unit] = {
|
def send(event: EventContext): F[Unit] =
|
||||||
|
event.withDefault(logger) { (title, body) =>
|
||||||
val url = Uri.unsafeFromString((channel.url / "message").asString)
|
val url = Uri.unsafeFromString((channel.url / "message").asString)
|
||||||
val req = POST(
|
val req = POST(
|
||||||
Json.obj(
|
Json.obj(
|
||||||
"title" -> Json.fromString(event.defaultTitle),
|
"title" -> Json.fromString(title),
|
||||||
"message" -> Json.fromString(event.defaultBody),
|
"message" -> Json.fromString(body),
|
||||||
"extras" -> Json.obj(
|
"extras" -> Json.obj(
|
||||||
"client::display" -> Json.obj(
|
"client::display" -> Json.obj(
|
||||||
"contentType" -> Json.fromString("text/markdown")
|
"contentType" -> Json.fromString("text/markdown")
|
||||||
|
@ -21,15 +21,17 @@ final class HttpPostBackend[F[_]: Async](
|
|||||||
channel: NotificationChannel.HttpPost,
|
channel: NotificationChannel.HttpPost,
|
||||||
client: Client[F],
|
client: Client[F],
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
) extends NotificationBackend[F] {
|
) extends NotificationBackend[F]
|
||||||
|
with EventContextSyntax {
|
||||||
|
|
||||||
val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {}
|
val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
import org.http4s.circe.CirceEntityCodec._
|
import org.http4s.circe.CirceEntityCodec._
|
||||||
|
|
||||||
def send(event: EventContext): F[Unit] = {
|
def send(event: EventContext): F[Unit] =
|
||||||
|
event.withJsonMessage(logger) { json =>
|
||||||
val url = Uri.unsafeFromString(channel.url.asString)
|
val url = Uri.unsafeFromString(channel.url.asString)
|
||||||
val req = POST(event.asJsonWithMessage, url).putHeaders(channel.headers.toList)
|
val req = POST(json, url).putHeaders(channel.headers.toList)
|
||||||
logger.debug(s"$channel sending request: $req") *>
|
logger.debug(s"$channel sending request: $req") *>
|
||||||
HttpSend.sendRequest(client, req, channel, logger)
|
HttpSend.sendRequest(client, req, channel, logger)
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,15 @@ final class MatrixBackend[F[_]: Async](
|
|||||||
channel: NotificationChannel.Matrix,
|
channel: NotificationChannel.Matrix,
|
||||||
client: Client[F],
|
client: Client[F],
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
) extends NotificationBackend[F] {
|
) extends NotificationBackend[F]
|
||||||
|
with EventContextSyntax {
|
||||||
|
|
||||||
val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {}
|
val dsl = new Http4sDsl[F] with Http4sClientDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
import org.http4s.circe.CirceEntityCodec._
|
import org.http4s.circe.CirceEntityCodec._
|
||||||
|
|
||||||
def send(event: EventContext): F[Unit] = {
|
def send(event: EventContext): F[Unit] =
|
||||||
|
event.withDefaultBoth(logger) { (md, html) =>
|
||||||
val url =
|
val url =
|
||||||
(channel.homeServer / "_matrix" / "client" / "r0" / "rooms" / channel.roomId / "send" / "m.room.message")
|
(channel.homeServer / "_matrix" / "client" / "r0" / "rooms" / channel.roomId / "send" / "m.room.message")
|
||||||
.withQuery("access_token", channel.accessToken.pass)
|
.withQuery("access_token", channel.accessToken.pass)
|
||||||
@ -35,8 +37,8 @@ final class MatrixBackend[F[_]: Async](
|
|||||||
Map(
|
Map(
|
||||||
"msgtype" -> channel.messageType,
|
"msgtype" -> channel.messageType,
|
||||||
"format" -> "org.matrix.custom.html",
|
"format" -> "org.matrix.custom.html",
|
||||||
"formatted_body" -> event.defaultBothHtml,
|
"formatted_body" -> html,
|
||||||
"body" -> event.defaultBoth
|
"body" -> md
|
||||||
),
|
),
|
||||||
uri
|
uri
|
||||||
)
|
)
|
||||||
|
@ -30,9 +30,9 @@ final case class DeleteFieldValueCtx(
|
|||||||
|
|
||||||
val content = data.asJson
|
val content = data.asJson
|
||||||
|
|
||||||
val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)"
|
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||||
val bodyTemplate =
|
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 content = data.asJson
|
||||||
|
|
||||||
val titleTemplate = mustache"Your items"
|
val titleTemplate = Right(mustache"Your items")
|
||||||
val bodyTemplate = mustache"""
|
val bodyTemplate = Right(mustache"""
|
||||||
Hello {{{ content.username }}},
|
Hello {{{ content.username }}},
|
||||||
|
|
||||||
this is Docspell informing you about your next items.
|
this is Docspell informing you about your next items.
|
||||||
@ -51,7 +51,7 @@ this is Docspell informing you about your next items.
|
|||||||
Sincerely yours,
|
Sincerely yours,
|
||||||
|
|
||||||
Docspell
|
Docspell
|
||||||
"""
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
object ItemSelectionCtx {
|
object ItemSelectionCtx {
|
||||||
@ -113,5 +113,4 @@ object ItemSelectionCtx {
|
|||||||
account.user.id
|
account.user.id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ final case class JobDoneCtx(event: Event.JobDone, data: JobDoneCtx.Data)
|
|||||||
|
|
||||||
val content = data.asJson
|
val content = data.asJson
|
||||||
|
|
||||||
val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)"
|
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||||
val bodyTemplate = mustache"""{{#content}}_'{{subject}}'_ finished {{/content}}"""
|
val bodyTemplate = Right(mustache"""{{#content}}_'{{subject}}'_ finished {{/content}}""")
|
||||||
}
|
}
|
||||||
|
|
||||||
object JobDoneCtx {
|
object JobDoneCtx {
|
||||||
|
@ -22,9 +22,9 @@ final case class JobSubmittedCtx(event: Event.JobSubmitted, data: JobSubmittedCt
|
|||||||
|
|
||||||
val content = data.asJson
|
val content = data.asJson
|
||||||
|
|
||||||
val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)"
|
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||||
val bodyTemplate =
|
val bodyTemplate =
|
||||||
mustache"""{{#content}}_'{{subject}}'_ submitted by {{submitter}} {{/content}}"""
|
Right(mustache"""{{#content}}_'{{subject}}'_ submitted by {{submitter}} {{/content}}""")
|
||||||
}
|
}
|
||||||
|
|
||||||
object JobSubmittedCtx {
|
object JobSubmittedCtx {
|
||||||
|
@ -28,9 +28,9 @@ final case class SetFieldValueCtx(event: Event.SetFieldValue, data: SetFieldValu
|
|||||||
|
|
||||||
val content = data.asJson
|
val content = data.asJson
|
||||||
|
|
||||||
val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)"
|
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||||
val bodyTemplate =
|
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 content = data.asJson
|
||||||
|
|
||||||
val titleTemplate = mustache"{{eventType}} (by *{{account.user}}*)"
|
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||||
val bodyTemplate =
|
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 {
|
object TagsChangedCtx {
|
||||||
|
@ -162,7 +162,11 @@ object NotificationRoutes {
|
|||||||
user.account,
|
user.account,
|
||||||
baseUrl.some
|
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
|
} yield resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user