mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +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:
@ -9,7 +9,7 @@
|
||||
|
||||
<logger name="docspell" level="debug" />
|
||||
<logger name="emil" level="debug"/>
|
||||
|
||||
<logger name="org.http4s.server.message-failures" level="debug"/>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
@ -6,11 +6,23 @@
|
||||
|
||||
package docspell.restserver
|
||||
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
|
||||
trait RestApp[F[_]] {
|
||||
|
||||
/** Access to the configuration used to build backend services. */
|
||||
def config: Config
|
||||
|
||||
/** Access to all backend services */
|
||||
def backend: BackendApp[F]
|
||||
|
||||
/** Stream consuming events (async) originating in this application. */
|
||||
def eventConsume(maxConcurrent: Int): Stream[F, Nothing]
|
||||
|
||||
/** Stream consuming messages from topics (pubsub) and forwarding them to the frontend
|
||||
* via websocket.
|
||||
*/
|
||||
def subscriptions: Stream[F, Nothing]
|
||||
}
|
||||
|
@ -7,18 +7,36 @@
|
||||
package docspell.restserver
|
||||
|
||||
import cats.effect._
|
||||
import fs2.Stream
|
||||
import fs2.concurrent.Topic
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.common.Logger
|
||||
import docspell.ftsclient.FtsClient
|
||||
import docspell.ftssolr.SolrFtsClient
|
||||
import docspell.notification.api.NotificationModule
|
||||
import docspell.notification.impl.NotificationModuleImpl
|
||||
import docspell.pubsub.api.{PubSub, PubSubT}
|
||||
import docspell.restserver.ws.OutputEvent
|
||||
import docspell.store.Store
|
||||
|
||||
import emil.javamail.JavaMailEmil
|
||||
import org.http4s.client.Client
|
||||
|
||||
final class RestAppImpl[F[_]](val config: Config, val backend: BackendApp[F])
|
||||
extends RestApp[F] {}
|
||||
final class RestAppImpl[F[_]: Async](
|
||||
val config: Config,
|
||||
val backend: BackendApp[F],
|
||||
notificationMod: NotificationModule[F],
|
||||
wsTopic: Topic[F, OutputEvent],
|
||||
pubSub: PubSubT[F]
|
||||
) extends RestApp[F] {
|
||||
|
||||
def eventConsume(maxConcurrent: Int): Stream[F, Nothing] =
|
||||
notificationMod.consumeAllEvents(maxConcurrent)
|
||||
|
||||
def subscriptions: Stream[F, Nothing] =
|
||||
Subscriptions[F](wsTopic, pubSub)
|
||||
}
|
||||
|
||||
object RestAppImpl {
|
||||
|
||||
@ -26,14 +44,21 @@ object RestAppImpl {
|
||||
cfg: Config,
|
||||
store: Store[F],
|
||||
httpClient: Client[F],
|
||||
pubSub: PubSub[F]
|
||||
pubSub: PubSub[F],
|
||||
wsTopic: Topic[F, OutputEvent]
|
||||
): Resource[F, RestApp[F]] = {
|
||||
val logger = Logger.log4s(org.log4s.getLogger(s"restserver-${cfg.appId.id}"))
|
||||
for {
|
||||
ftsClient <- createFtsClient(cfg)(httpClient)
|
||||
pubSubT = PubSubT(pubSub, logger)
|
||||
backend <- BackendApp.create[F](cfg.backend, store, ftsClient, pubSubT)
|
||||
app = new RestAppImpl[F](cfg, backend)
|
||||
javaEmil = JavaMailEmil(cfg.backend.mailSettings)
|
||||
notificationMod <- Resource.eval(
|
||||
NotificationModuleImpl[F](store, javaEmil, httpClient, 200)
|
||||
)
|
||||
backend <- BackendApp
|
||||
.create[F](store, javaEmil, ftsClient, pubSubT, notificationMod)
|
||||
|
||||
app = new RestAppImpl[F](cfg, backend, notificationMod, wsTopic, pubSubT)
|
||||
} yield app
|
||||
}
|
||||
|
||||
|
@ -50,10 +50,11 @@ object RestServer {
|
||||
|
||||
server =
|
||||
Stream
|
||||
.resource(createApp(cfg, pools))
|
||||
.resource(createApp(cfg, pools, wsTopic))
|
||||
.flatMap { case (restApp, pubSub, httpClient, setting) =>
|
||||
Stream(
|
||||
Subscriptions(wsTopic, restApp.backend.pubSub),
|
||||
restApp.subscriptions,
|
||||
restApp.eventConsume(2),
|
||||
BlazeServerBuilder[F]
|
||||
.bindHttp(cfg.bind.port, cfg.bind.address)
|
||||
.withoutBanner
|
||||
@ -71,8 +72,12 @@ object RestServer {
|
||||
|
||||
def createApp[F[_]: Async](
|
||||
cfg: Config,
|
||||
pools: Pools
|
||||
): Resource[F, (RestApp[F], NaivePubSub[F], Client[F], RInternalSetting)] =
|
||||
pools: Pools,
|
||||
wsTopic: Topic[F, OutputEvent]
|
||||
): Resource[
|
||||
F,
|
||||
(RestApp[F], NaivePubSub[F], Client[F], RInternalSetting)
|
||||
] =
|
||||
for {
|
||||
httpClient <- BlazeClientBuilder[F].resource
|
||||
store <- Store.create[F](
|
||||
@ -86,7 +91,7 @@ object RestServer {
|
||||
store,
|
||||
httpClient
|
||||
)(Topics.all.map(_.topic))
|
||||
restApp <- RestAppImpl.create[F](cfg, store, httpClient, pubSub)
|
||||
restApp <- RestAppImpl.create[F](cfg, store, httpClient, pubSub, wsTopic)
|
||||
} yield (restApp, pubSub, httpClient, setting)
|
||||
|
||||
def createHttpApp[F[_]: Async](
|
||||
@ -150,7 +155,7 @@ object RestServer {
|
||||
"collective" -> CollectiveRoutes(restApp.backend, token),
|
||||
"queue" -> JobQueueRoutes(restApp.backend, token),
|
||||
"item" -> ItemRoutes(cfg, restApp.backend, token),
|
||||
"items" -> ItemMultiRoutes(restApp.backend, token),
|
||||
"items" -> ItemMultiRoutes(cfg, restApp.backend, token),
|
||||
"attachment" -> AttachmentRoutes(restApp.backend, token),
|
||||
"attachments" -> AttachmentMultiRoutes(restApp.backend, token),
|
||||
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
|
||||
@ -161,11 +166,13 @@ object RestServer {
|
||||
"share" -> ShareRoutes.manage(restApp.backend, token),
|
||||
"usertask/notifydueitems" -> NotifyDueItemsRoutes(cfg, restApp.backend, token),
|
||||
"usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token),
|
||||
"usertask/periodicquery" -> PeriodicQueryRoutes(cfg, restApp.backend, token),
|
||||
"calevent/check" -> CalEventCheckRoutes(),
|
||||
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token),
|
||||
"folder" -> FolderRoutes(restApp.backend, token),
|
||||
"customfield" -> CustomFieldRoutes(restApp.backend, token),
|
||||
"clientSettings" -> ClientSettingsRoutes(restApp.backend, token)
|
||||
"clientSettings" -> ClientSettingsRoutes(restApp.backend, token),
|
||||
"notification" -> NotificationRoutes(cfg, restApp.backend, token)
|
||||
)
|
||||
|
||||
def openRoutes[F[_]: Async](
|
||||
|
@ -14,7 +14,9 @@ import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue}
|
||||
import docspell.common._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.{Conversions, MultiIdSupport}
|
||||
import docspell.restserver.http4s.ClientRequestInfo
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
@ -26,6 +28,7 @@ object ItemMultiRoutes extends MultiIdSupport {
|
||||
private[this] val log4sLogger = getLogger
|
||||
|
||||
def apply[F[_]: Async](
|
||||
cfg: Config,
|
||||
backend: BackendApp[F],
|
||||
user: AuthToken
|
||||
): HttpRoutes[F] = {
|
||||
@ -66,7 +69,9 @@ object ItemMultiRoutes extends MultiIdSupport {
|
||||
json.refs,
|
||||
user.account.collective
|
||||
)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tags updated"))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Tags updated"))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "tags" =>
|
||||
@ -78,7 +83,9 @@ object ItemMultiRoutes extends MultiIdSupport {
|
||||
json.refs,
|
||||
user.account.collective
|
||||
)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tags added."))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Tags added."))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "tagsremove" =>
|
||||
@ -90,7 +97,9 @@ object ItemMultiRoutes extends MultiIdSupport {
|
||||
json.refs,
|
||||
user.account.collective
|
||||
)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tags removed"))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Tags removed"))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root / "name" =>
|
||||
@ -205,7 +214,9 @@ object ItemMultiRoutes extends MultiIdSupport {
|
||||
items,
|
||||
SetValue(json.field.field, json.field.value, user.account.collective)
|
||||
)
|
||||
resp <- Ok(Conversions.basicResult(res))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "customfieldremove" =>
|
||||
@ -216,7 +227,9 @@ object ItemMultiRoutes extends MultiIdSupport {
|
||||
res <- backend.customFields.deleteValue(
|
||||
RemoveValue(field, items, user.account.collective)
|
||||
)
|
||||
resp <- Ok(Conversions.basicResult(res, "Custom fields removed."))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Custom fields removed."))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "merge" =>
|
||||
|
@ -25,6 +25,7 @@ import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s.BinaryUtil
|
||||
import docspell.restserver.http4s.ClientRequestInfo
|
||||
import docspell.restserver.http4s.Responses
|
||||
import docspell.restserver.http4s.{QueryParam => QP}
|
||||
|
||||
@ -160,29 +161,37 @@ object ItemRoutes {
|
||||
for {
|
||||
tags <- req.as[StringList].map(_.items)
|
||||
res <- backend.item.setTags(id, tags, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tags updated"))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Tags updated"))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / Ident(id) / "tags" =>
|
||||
for {
|
||||
data <- req.as[Tag]
|
||||
rtag <- Conversions.newTag(data, user.account.collective)
|
||||
res <- backend.item.addNewTag(id, rtag)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tag added."))
|
||||
res <- backend.item.addNewTag(user.account.collective, id, rtag)
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Tag added."))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root / Ident(id) / "taglink" =>
|
||||
for {
|
||||
tags <- req.as[StringList]
|
||||
res <- backend.item.linkTags(id, tags.items, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tags linked"))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Tags linked"))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / Ident(id) / "tagtoggle" =>
|
||||
for {
|
||||
tags <- req.as[StringList]
|
||||
res <- backend.item.toggleTags(id, tags.items, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tags linked"))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Tags linked"))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / Ident(id) / "tagsremove" =>
|
||||
@ -193,7 +202,9 @@ object ItemRoutes {
|
||||
json.items,
|
||||
user.account.collective
|
||||
)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tags removed"))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Tags removed"))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root / Ident(id) / "direction" =>
|
||||
@ -392,15 +403,19 @@ object ItemRoutes {
|
||||
id,
|
||||
SetValue(data.field, data.value, user.account.collective)
|
||||
)
|
||||
resp <- Ok(Conversions.basicResult(res))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) / "customfield" / Ident(fieldId) =>
|
||||
case req @ DELETE -> Root / Ident(id) / "customfield" / Ident(fieldId) =>
|
||||
for {
|
||||
res <- backend.customFields.deleteValue(
|
||||
RemoveValue(fieldId, NonEmptyList.of(id), user.account.collective)
|
||||
)
|
||||
resp <- Ok(Conversions.basicResult(res, "Custom field value removed."))
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
_ <- backend.notification.offerEvents(res.event(user.account, baseUrl.some))
|
||||
resp <- Ok(Conversions.basicResult(res.value, "Custom field value removed."))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
|
@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common._
|
||||
import docspell.joexapi.model.BasicResult
|
||||
import docspell.jsonminiq.JsonMiniQuery
|
||||
import docspell.notification.api.EventType
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s.ClientRequestInfo
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.server.Router
|
||||
|
||||
object NotificationRoutes {
|
||||
|
||||
def apply[F[_]: Async](
|
||||
cfg: Config,
|
||||
backend: BackendApp[F],
|
||||
user: AuthToken
|
||||
): HttpRoutes[F] =
|
||||
Router(
|
||||
"channel" -> channels(backend, user),
|
||||
"hook" -> hooks(cfg, backend, user),
|
||||
"event" -> events(cfg, backend, user)
|
||||
)
|
||||
|
||||
def channels[F[_]: Async](
|
||||
backend: BackendApp[F],
|
||||
user: AuthToken
|
||||
): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root =>
|
||||
for {
|
||||
list <- backend.notification.listChannels(user.account)
|
||||
data = list.map(NotificationChannel.convert)
|
||||
resp <- Ok(data)
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
res <- backend.notification.deleteChannel(id, user.account)
|
||||
resp <- Ok(Conversions.basicResult(res, "Channel deleted"))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
input <- req.as[NotificationChannel]
|
||||
ch <- Sync[F].pure(NotificationChannel.convert(input)).rethrow
|
||||
res <- backend.notification.createChannel(ch, user.account)
|
||||
resp <- Ok(Conversions.basicResult(res, "Channel created"))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
for {
|
||||
input <- req.as[NotificationChannel]
|
||||
ch <- Sync[F].pure(NotificationChannel.convert(input)).rethrow
|
||||
res <- backend.notification.updateChannel(ch, user.account)
|
||||
resp <- Ok(Conversions.basicResult(res, "Channel created"))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
def hooks[F[_]: Async](
|
||||
cfg: Config,
|
||||
backend: BackendApp[F],
|
||||
user: AuthToken
|
||||
): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root =>
|
||||
for {
|
||||
list <- backend.notification.listHooks(user.account)
|
||||
data = list.map(Converters.convertHook)
|
||||
resp <- Ok(data)
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
res <- backend.notification.deleteHook(id, user.account)
|
||||
resp <- Ok(Conversions.basicResult(res, "Hook deleted."))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
input <- req.as[NotificationHook]
|
||||
hook <- Sync[F].pure(Converters.convertHook(input)).rethrow
|
||||
res <- backend.notification.createHook(hook, user.account)
|
||||
resp <- Ok(Conversions.basicResult(res, "Hook created"))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
for {
|
||||
input <- req.as[NotificationHook]
|
||||
hook <- Sync[F].pure(Converters.convertHook(input)).rethrow
|
||||
res <- backend.notification.updateHook(hook, user.account)
|
||||
resp <- Ok(Conversions.basicResult(res, "Hook updated"))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "verifyJsonFilter" =>
|
||||
for {
|
||||
input <- req.as[StringValue]
|
||||
res = JsonMiniQuery.parse(input.value)
|
||||
resp <- Ok(BasicResult(res.isRight, res.fold(identity, _.unsafeAsString)))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "sendTestEvent" =>
|
||||
for {
|
||||
input <- req.as[NotificationHook]
|
||||
ch <- Sync[F]
|
||||
.pure(
|
||||
input.channel.left
|
||||
.map(_ => new Exception(s"ChannelRefs not allowed for testing"))
|
||||
.flatMap(NotificationChannel.convert)
|
||||
)
|
||||
.rethrow
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
res <- backend.notification.sendSampleEvent(
|
||||
input.events.headOption.getOrElse(EventType.all.head),
|
||||
ch,
|
||||
user.account,
|
||||
baseUrl.some
|
||||
)
|
||||
resp <- Ok(NotificationChannelTestResult(res.success, res.logMessages.toList))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
def events[F[_]: Async](
|
||||
cfg: Config,
|
||||
backend: BackendApp[F],
|
||||
user: AuthToken
|
||||
): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of { case req @ POST -> Root / "sample" =>
|
||||
for {
|
||||
input <- req.as[NotificationSampleEventReq]
|
||||
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
data <- backend.notification.sampleEvent(
|
||||
input.eventType,
|
||||
user.account,
|
||||
baseUrl.some
|
||||
)
|
||||
resp <- Ok(data.asJsonWithMessage)
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
object Converters {
|
||||
|
||||
import docspell.backend.ops.ONotification
|
||||
|
||||
def convertHook(h: ONotification.Hook): NotificationHook =
|
||||
NotificationHook(
|
||||
h.id,
|
||||
h.enabled,
|
||||
h.channel.map(NotificationChannel.convert),
|
||||
h.allEvents,
|
||||
h.eventFilter,
|
||||
h.events
|
||||
)
|
||||
|
||||
def convertHook(h: NotificationHook): Either[Throwable, ONotification.Hook] =
|
||||
h.channel match {
|
||||
case Left(cref) =>
|
||||
Right(
|
||||
ONotification.Hook(
|
||||
h.id,
|
||||
h.enabled,
|
||||
Left(cref),
|
||||
h.allEvents,
|
||||
h.eventFilter,
|
||||
h.events
|
||||
)
|
||||
)
|
||||
case Right(channel) =>
|
||||
NotificationChannel
|
||||
.convert(channel)
|
||||
.map(ch =>
|
||||
ONotification
|
||||
.Hook(h.id, h.enabled, Right(ch), h.allEvents, h.eventFilter, h.events)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -11,8 +11,10 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.MailAddressCodec
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common._
|
||||
import docspell.notification.api.PeriodicDueItemsArgs
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
@ -24,7 +26,7 @@ import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object NotifyDueItemsRoutes {
|
||||
object NotifyDueItemsRoutes extends MailAddressCodec {
|
||||
|
||||
def apply[F[_]: Async](
|
||||
cfg: Config,
|
||||
@ -39,13 +41,13 @@ object NotifyDueItemsRoutes {
|
||||
case GET -> Root / Ident(id) =>
|
||||
(for {
|
||||
task <- ut.findNotifyDueItems(id, UserTaskScope(user.account))
|
||||
res <- OptionT.liftF(taskToSettings(user.account, backend, task))
|
||||
res <- OptionT.liftF(taskToSettings(backend, task))
|
||||
resp <- OptionT.liftF(Ok(res))
|
||||
} yield resp).getOrElseF(NotFound())
|
||||
|
||||
case req @ POST -> Root / "startonce" =>
|
||||
for {
|
||||
data <- req.as[NotificationSettings]
|
||||
data <- req.as[PeriodicDueItemsSettings]
|
||||
newId <- Ident.randomId[F]
|
||||
task <- makeTask(newId, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
@ -65,7 +67,7 @@ object NotifyDueItemsRoutes {
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
def run(data: NotificationSettings) =
|
||||
def run(data: PeriodicDueItemsSettings) =
|
||||
for {
|
||||
task <- makeTask(data.id, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
@ -75,7 +77,7 @@ object NotifyDueItemsRoutes {
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
for {
|
||||
data <- req.as[NotificationSettings]
|
||||
data <- req.as[PeriodicDueItemsSettings]
|
||||
resp <-
|
||||
if (data.id.isEmpty) Ok(BasicResult(false, "Empty id is not allowed"))
|
||||
else run(data)
|
||||
@ -83,7 +85,7 @@ object NotifyDueItemsRoutes {
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[NotificationSettings]
|
||||
data <- req.as[PeriodicDueItemsSettings]
|
||||
newId <- Ident.randomId[F]
|
||||
task <- makeTask(newId, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
@ -95,10 +97,9 @@ object NotifyDueItemsRoutes {
|
||||
|
||||
case GET -> Root =>
|
||||
ut.getNotifyDueItems(UserTaskScope(user.account))
|
||||
.evalMap(task => taskToSettings(user.account, backend, task))
|
||||
.evalMap(task => taskToSettings(backend, task))
|
||||
.compile
|
||||
.toVector
|
||||
.map(v => NotificationSettingsList(v.toList))
|
||||
.flatMap(Ok(_))
|
||||
}
|
||||
}
|
||||
@ -110,50 +111,49 @@ object NotifyDueItemsRoutes {
|
||||
id: Ident,
|
||||
baseUrl: LenientUri,
|
||||
user: AccountId,
|
||||
settings: NotificationSettings
|
||||
): F[UserTask[NotifyDueItemsArgs]] =
|
||||
Sync[F].pure(
|
||||
settings: PeriodicDueItemsSettings
|
||||
): F[UserTask[PeriodicDueItemsArgs]] =
|
||||
Sync[F].pure(NotificationChannel.convert(settings.channel)).rethrow.map { channel =>
|
||||
UserTask(
|
||||
id,
|
||||
NotifyDueItemsArgs.taskName,
|
||||
PeriodicDueItemsArgs.taskName,
|
||||
settings.enabled,
|
||||
settings.schedule,
|
||||
settings.summary,
|
||||
NotifyDueItemsArgs(
|
||||
PeriodicDueItemsArgs(
|
||||
user,
|
||||
settings.smtpConnection,
|
||||
settings.recipients,
|
||||
Some(baseUrl / "app" / "item"),
|
||||
Right(channel),
|
||||
settings.remindDays,
|
||||
if (settings.capOverdue) Some(settings.remindDays)
|
||||
else None,
|
||||
settings.tagsInclude.map(_.id),
|
||||
settings.tagsExclude.map(_.id)
|
||||
settings.tagsExclude.map(_.id),
|
||||
Some(baseUrl / "app" / "item")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def taskToSettings[F[_]: Sync](
|
||||
account: AccountId,
|
||||
backend: BackendApp[F],
|
||||
task: UserTask[NotifyDueItemsArgs]
|
||||
): F[NotificationSettings] =
|
||||
task: UserTask[PeriodicDueItemsArgs]
|
||||
): F[PeriodicDueItemsSettings] =
|
||||
for {
|
||||
tinc <- backend.tag.loadAll(task.args.tagsInclude)
|
||||
texc <- backend.tag.loadAll(task.args.tagsExclude)
|
||||
conn <-
|
||||
backend.mail
|
||||
.getSmtpSettings(account, None)
|
||||
.map(
|
||||
_.find(_.name == task.args.smtpConnection)
|
||||
.map(_.name)
|
||||
|
||||
ch <- task.args.channel match {
|
||||
case Right(c) => NotificationChannel.convert(c).pure[F]
|
||||
case Left(ref) =>
|
||||
Sync[F].raiseError(
|
||||
new IllegalStateException(s"ChannelRefs are not supported: $ref")
|
||||
)
|
||||
} yield NotificationSettings(
|
||||
}
|
||||
|
||||
} yield PeriodicDueItemsSettings(
|
||||
task.id,
|
||||
task.enabled,
|
||||
task.summary,
|
||||
conn.getOrElse(Ident.unsafe("")),
|
||||
task.args.recipients,
|
||||
ch,
|
||||
task.timer,
|
||||
task.args.remindDays,
|
||||
task.args.daysBack.isDefined,
|
||||
|
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.MailAddressCodec
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common._
|
||||
import docspell.notification.api.PeriodicQueryArgs
|
||||
import docspell.query.ItemQueryParser
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s.ClientRequestInfo
|
||||
import docspell.store.usertask._
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object PeriodicQueryRoutes extends MailAddressCodec {
|
||||
|
||||
def apply[F[_]: Async](
|
||||
cfg: Config,
|
||||
backend: BackendApp[F],
|
||||
user: AuthToken
|
||||
): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
val ut = backend.userTask
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root / Ident(id) =>
|
||||
(for {
|
||||
task <- ut.findPeriodicQuery(id, UserTaskScope(user.account))
|
||||
res <- OptionT.liftF(taskToSettings(task))
|
||||
resp <- OptionT.liftF(Ok(res))
|
||||
} yield resp).getOrElseF(NotFound())
|
||||
|
||||
case req @ POST -> Root / "startonce" =>
|
||||
for {
|
||||
data <- req.as[PeriodicQuerySettings]
|
||||
newId <- Ident.randomId[F]
|
||||
task <- makeTask(newId, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
ut.executeNow(UserTaskScope(user.account), None, task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Submitted successfully."))
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
res <-
|
||||
ut.deleteTask(UserTaskScope(user.account), id)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Deleted successfully"))
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
def run(data: PeriodicQuerySettings) =
|
||||
for {
|
||||
task <- makeTask(data.id, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
ut.submitPeriodicQuery(UserTaskScope(user.account), None, task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Saved successfully"))
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
for {
|
||||
data <- req.as[PeriodicQuerySettings]
|
||||
resp <-
|
||||
if (data.id.isEmpty) Ok(BasicResult(false, "Empty id is not allowed"))
|
||||
else run(data)
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[PeriodicQuerySettings]
|
||||
newId <- Ident.randomId[F]
|
||||
task <- makeTask(newId, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
ut.submitPeriodicQuery(UserTaskScope(user.account), None, task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Saved successfully."))
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
|
||||
case GET -> Root =>
|
||||
ut.getPeriodicQuery(UserTaskScope(user.account))
|
||||
.evalMap(task => taskToSettings(task))
|
||||
.compile
|
||||
.toVector
|
||||
.flatMap(Ok(_))
|
||||
}
|
||||
}
|
||||
|
||||
private def getBaseUrl[F[_]](cfg: Config, req: Request[F]) =
|
||||
ClientRequestInfo.getBaseUrl(cfg, req)
|
||||
|
||||
def makeTask[F[_]: Sync](
|
||||
id: Ident,
|
||||
baseUrl: LenientUri,
|
||||
user: AccountId,
|
||||
settings: PeriodicQuerySettings
|
||||
): F[UserTask[PeriodicQueryArgs]] =
|
||||
Sync[F]
|
||||
.pure(for {
|
||||
ch <- NotificationChannel.convert(settings.channel)
|
||||
qstr <- ItemQueryParser
|
||||
.asString(settings.query.expr)
|
||||
.left
|
||||
.map(err => new IllegalArgumentException(s"Query not renderable: $err"))
|
||||
} yield (ch, ItemQueryString(qstr)))
|
||||
.rethrow
|
||||
.map { case (channel, qstr) =>
|
||||
UserTask(
|
||||
id,
|
||||
PeriodicQueryArgs.taskName,
|
||||
settings.enabled,
|
||||
settings.schedule,
|
||||
settings.summary,
|
||||
PeriodicQueryArgs(
|
||||
user,
|
||||
Right(channel),
|
||||
qstr,
|
||||
Some(baseUrl / "app" / "item")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def taskToSettings[F[_]: Sync](
|
||||
task: UserTask[PeriodicQueryArgs]
|
||||
): F[PeriodicQuerySettings] =
|
||||
for {
|
||||
ch <- task.args.channel match {
|
||||
case Right(c) => NotificationChannel.convert(c).pure[F]
|
||||
case Left(ref) =>
|
||||
Sync[F].raiseError(
|
||||
new IllegalStateException(s"ChannelRefs are not supported: $ref")
|
||||
)
|
||||
}
|
||||
} yield PeriodicQuerySettings(
|
||||
task.id,
|
||||
task.summary,
|
||||
task.enabled,
|
||||
ch,
|
||||
ItemQueryParser.parseUnsafe(task.args.query.query),
|
||||
task.timer
|
||||
)
|
||||
}
|
@ -57,7 +57,6 @@ object OutputEvent {
|
||||
|
||||
private case class Msg[A](tag: String, content: A)
|
||||
private object Msg {
|
||||
@scala.annotation.nowarn
|
||||
implicit def jsonEncoder[A: Encoder]: Encoder[Msg[A]] =
|
||||
deriveEncoder
|
||||
}
|
||||
|
Reference in New Issue
Block a user