mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 10:28:27 +00:00
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:
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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]
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user