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

View File

@ -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>

View File

@ -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]
}

View File

@ -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
}

View File

@ -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](

View File

@ -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" =>

View File

@ -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) =>

View File

@ -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)
)
}
}
}

View File

@ -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,

View File

@ -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
)
}

View File

@ -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
}