Add support for more generic notification

This is a start to have different kinds of notifications. It is
possible to be notified via e-mail, matrix or gotify. It also extends
the current "periodic query" for due items by allowing notification
over different channels. A "generic periodic query" variant is added
as well.
This commit is contained in:
eikek
2021-11-22 00:22:51 +01:00
parent 93a828720c
commit 4ffc8d1f14
175 changed files with 13041 additions and 599 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.restapi.codec
import docspell.notification.api.ChannelRef
import docspell.restapi.model._
import io.circe.syntax._
import io.circe.{Decoder, Encoder}
trait ChannelEitherCodec {
implicit val channelDecoder: Decoder[Either[ChannelRef, NotificationChannel]] =
NotificationChannel.jsonDecoder.either(ChannelRef.jsonDecoder).map(_.swap)
implicit val channelEncoder: Encoder[Either[ChannelRef, NotificationChannel]] =
Encoder.instance(_.fold(_.asJson, _.asJson))
}
object ChannelEitherCodec extends ChannelEitherCodec

View File

@ -0,0 +1,130 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.restapi.model
import cats.data.NonEmptyList
import cats.implicits._
import docspell.notification.api.Channel
import docspell.notification.api.ChannelType
import docspell.restapi.model._
import emil.MailAddress
import emil.javamail.syntax._
import io.circe.{Decoder, Encoder}
sealed trait NotificationChannel {
def fold[A](
f1: NotificationMail => A,
f2: NotificationGotify => A,
f3: NotificationMatrix => A,
f4: NotificationHttp => A
): A
}
object NotificationChannel {
final case class Mail(c: NotificationMail) extends NotificationChannel {
def fold[A](
f1: NotificationMail => A,
f2: NotificationGotify => A,
f3: NotificationMatrix => A,
f4: NotificationHttp => A
): A = f1(c)
}
final case class Gotify(c: NotificationGotify) extends NotificationChannel {
def fold[A](
f1: NotificationMail => A,
f2: NotificationGotify => A,
f3: NotificationMatrix => A,
f4: NotificationHttp => A
): A = f2(c)
}
final case class Matrix(c: NotificationMatrix) extends NotificationChannel {
def fold[A](
f1: NotificationMail => A,
f2: NotificationGotify => A,
f3: NotificationMatrix => A,
f4: NotificationHttp => A
): A = f3(c)
}
final case class Http(c: NotificationHttp) extends NotificationChannel {
def fold[A](
f1: NotificationMail => A,
f2: NotificationGotify => A,
f3: NotificationMatrix => A,
f4: NotificationHttp => A
): A = f4(c)
}
def mail(c: NotificationMail): NotificationChannel = Mail(c)
def gotify(c: NotificationGotify): NotificationChannel = Gotify(c)
def matrix(c: NotificationMatrix): NotificationChannel = Matrix(c)
def http(c: NotificationHttp): NotificationChannel = Http(c)
def convert(c: NotificationChannel): Either[Throwable, Channel] =
c.fold(
mail =>
mail.recipients
.traverse(MailAddress.parse)
.map(NonEmptyList.fromList)
.flatMap(_.toRight("No recipients given!"))
.leftMap(new IllegalArgumentException(_))
.map(rec => Channel.Mail(mail.id, mail.connection, rec)),
gotify => Right(Channel.Gotify(gotify.id, gotify.url, gotify.appKey)),
matrix =>
Right(
Channel
.Matrix(matrix.id, matrix.homeServer, matrix.roomId, matrix.accessToken)
),
http => Right(Channel.Http(http.id, http.url))
)
def convert(c: Channel): NotificationChannel =
c.fold(
m =>
mail {
NotificationMail(
m.id,
ChannelType.Mail,
m.connection,
m.recipients.toList.map(_.displayString)
)
},
g => gotify(NotificationGotify(g.id, ChannelType.Gotify, g.url, g.appKey)),
m =>
matrix(
NotificationMatrix(
m.id,
ChannelType.Matrix,
m.homeServer,
m.roomId,
m.accessToken
)
),
h => http(NotificationHttp(h.id, ChannelType.Http, h.url))
)
implicit val jsonDecoder: Decoder[NotificationChannel] =
ChannelType.jsonDecoder.at("channelType").flatMap {
case ChannelType.Mail => Decoder[NotificationMail].map(mail)
case ChannelType.Gotify => Decoder[NotificationGotify].map(gotify)
case ChannelType.Matrix => Decoder[NotificationMatrix].map(matrix)
case ChannelType.Http => Decoder[NotificationHttp].map(http)
}
implicit val jsonEncoder: Encoder[NotificationChannel] =
Encoder.instance {
case NotificationChannel.Mail(c) =>
Encoder[NotificationMail].apply(c)
case NotificationChannel.Gotify(c) =>
Encoder[NotificationGotify].apply(c)
case NotificationChannel.Matrix(c) =>
Encoder[NotificationMatrix].apply(c)
case NotificationChannel.Http(c) =>
Encoder[NotificationHttp].apply(c)
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.restapi.model
import docspell.common._
import docspell.jsonminiq.JsonMiniQuery
import docspell.notification.api.{ChannelRef, EventType}
import docspell.restapi.codec.ChannelEitherCodec
import io.circe.{Decoder, Encoder}
// this must comply to the definition in openapi.yml in `extraSchemas`
final case class NotificationHook(
id: Ident,
enabled: Boolean,
channel: Either[ChannelRef, NotificationChannel],
allEvents: Boolean,
eventFilter: Option[JsonMiniQuery],
events: List[EventType]
)
object NotificationHook {
import ChannelEitherCodec._
implicit val jsonDecoder: Decoder[NotificationHook] =
io.circe.generic.semiauto.deriveDecoder
implicit val jsonEncoder: Encoder[NotificationHook] =
io.circe.generic.semiauto.deriveEncoder
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.restapi.model
import docspell.common._
import docspell.restapi.model._
import com.github.eikek.calev.CalEvent
import com.github.eikek.calev.circe.CalevCirceCodec._
import io.circe.generic.semiauto
import io.circe.{Decoder, Encoder}
// this must comply to the definition in openapi.yml in `extraSchemas`
final case class PeriodicDueItemsSettings(
id: Ident,
enabled: Boolean,
summary: Option[String],
channel: NotificationChannel,
schedule: CalEvent,
remindDays: Int,
capOverdue: Boolean,
tagsInclude: List[Tag],
tagsExclude: List[Tag]
)
object PeriodicDueItemsSettings {
implicit val jsonDecoder: Decoder[PeriodicDueItemsSettings] =
semiauto.deriveDecoder[PeriodicDueItemsSettings]
implicit val jsonEncoder: Encoder[PeriodicDueItemsSettings] =
semiauto.deriveEncoder[PeriodicDueItemsSettings]
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.restapi.model
import docspell.common._
import docspell.query.ItemQuery
import docspell.restapi.codec.ItemQueryJson._
import com.github.eikek.calev.CalEvent
import com.github.eikek.calev.circe.CalevCirceCodec._
import io.circe.generic.semiauto
import io.circe.{Decoder, Encoder}
// this must comply to the definition in openapi.yml in `extraSchemas`
final case class PeriodicQuerySettings(
id: Ident,
summary: Option[String],
enabled: Boolean,
channel: NotificationChannel,
query: ItemQuery,
schedule: CalEvent
) {}
object PeriodicQuerySettings {
implicit val jsonDecoder: Decoder[PeriodicQuerySettings] =
semiauto.deriveDecoder
implicit val jsonEncoder: Encoder[PeriodicQuerySettings] =
semiauto.deriveEncoder
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.restapi.model
import docspell.common._
import docspell.notification.api.ChannelRef
import docspell.notification.api.ChannelType
import io.circe.Decoder
import io.circe.parser
import munit._
class NotificationCodecTest extends FunSuite {
def parse[A: Decoder](str: String): A =
parser.parse(str).fold(throw _, identity).as[A].fold(throw _, identity)
def id(str: String): Ident =
Ident.unsafe(str)
test("decode with channelref") {
val json = """{"id":"",
"enabled": true,
"channel": {"id":"abcde", "channelType":"matrix"},
"allEvents": false,
"events": ["TagsChanged", "SetFieldValue"]
}"""
val hook = parse[NotificationHook](json)
assertEquals(hook.enabled, true)
assertEquals(hook.channel, Left(ChannelRef(id("abcde"), ChannelType.Matrix)))
}
test("decode with gotify data") {
val json = """{"id":"",
"enabled": true,
"channel": {"id":"", "channelType":"gotify", "url":"http://test.gotify.com", "appKey": "abcde"},
"allEvents": false,
"eventFilter": null,
"events": ["TagsChanged", "SetFieldValue"]
}"""
val hook = parse[NotificationHook](json)
assertEquals(hook.enabled, true)
assertEquals(
hook.channel,
Right(
NotificationChannel.Gotify(
NotificationGotify(
id(""),
ChannelType.Gotify,
LenientUri.unsafe("http://test.gotify.com"),
Password("abcde")
)
)
)
)
}
}