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:
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.backend
|
||||
|
||||
import docspell.common._
|
||||
import docspell.notification.api.Event
|
||||
|
||||
trait AttachedEvent[R] {
|
||||
|
||||
def value: R
|
||||
|
||||
def event(account: AccountId, baseUrl: Option[LenientUri]): Iterable[Event]
|
||||
|
||||
def map[U](f: R => U): AttachedEvent[U]
|
||||
}
|
||||
|
||||
object AttachedEvent {
|
||||
|
||||
def only[R](v: R): AttachedEvent[R] =
|
||||
new AttachedEvent[R] {
|
||||
val value = v
|
||||
def event(account: AccountId, baseUrl: Option[LenientUri]): Iterable[Event] =
|
||||
Iterable.empty[Event]
|
||||
|
||||
def map[U](f: R => U): AttachedEvent[U] =
|
||||
only(f(v))
|
||||
}
|
||||
|
||||
def apply[R](
|
||||
v: R
|
||||
)(mkEvent: (AccountId, Option[LenientUri]) => Event): AttachedEvent[R] =
|
||||
new AttachedEvent[R] {
|
||||
val value = v
|
||||
def event(account: AccountId, baseUrl: Option[LenientUri]): Iterable[Event] =
|
||||
Some(mkEvent(account, baseUrl))
|
||||
|
||||
def map[U](f: R => U): AttachedEvent[U] =
|
||||
apply(f(v))(mkEvent)
|
||||
}
|
||||
}
|
@ -14,12 +14,13 @@ import docspell.backend.msg.JobQueuePublish
|
||||
import docspell.backend.ops._
|
||||
import docspell.backend.signup.OSignup
|
||||
import docspell.ftsclient.FtsClient
|
||||
import docspell.notification.api.{EventExchange, NotificationModule}
|
||||
import docspell.pubsub.api.PubSubT
|
||||
import docspell.store.Store
|
||||
import docspell.store.usertask.UserTaskStore
|
||||
import docspell.totp.Totp
|
||||
|
||||
import emil.javamail.{JavaMailEmil, Settings}
|
||||
import emil.Emil
|
||||
|
||||
trait BackendApp[F[_]] {
|
||||
|
||||
@ -46,19 +47,22 @@ trait BackendApp[F[_]] {
|
||||
def totp: OTotp[F]
|
||||
def share: OShare[F]
|
||||
def pubSub: PubSubT[F]
|
||||
def events: EventExchange[F]
|
||||
def notification: ONotification[F]
|
||||
}
|
||||
|
||||
object BackendApp {
|
||||
|
||||
def create[F[_]: Async](
|
||||
cfg: Config,
|
||||
store: Store[F],
|
||||
javaEmil: Emil[F],
|
||||
ftsClient: FtsClient[F],
|
||||
pubSubT: PubSubT[F]
|
||||
pubSubT: PubSubT[F],
|
||||
notificationMod: NotificationModule[F]
|
||||
): Resource[F, BackendApp[F]] =
|
||||
for {
|
||||
utStore <- UserTaskStore(store)
|
||||
queue <- JobQueuePublish(store, pubSubT)
|
||||
queue <- JobQueuePublish(store, pubSubT, notificationMod)
|
||||
totpImpl <- OTotp(store, Totp.default)
|
||||
loginImpl <- Login[F](store, Totp.default)
|
||||
signupImpl <- OSignup[F](store)
|
||||
@ -75,8 +79,6 @@ object BackendApp {
|
||||
itemImpl <- OItem(store, ftsClient, createIndex, queue, joexImpl)
|
||||
itemSearchImpl <- OItemSearch(store)
|
||||
fulltextImpl <- OFulltext(itemSearchImpl, ftsClient, store, queue, joexImpl)
|
||||
javaEmil =
|
||||
JavaMailEmil(Settings.defaultSettings.copy(debug = cfg.mailDebug))
|
||||
mailImpl <- OMail(store, javaEmil)
|
||||
userTaskImpl <- OUserTask(utStore, queue, joexImpl)
|
||||
folderImpl <- OFolder(store)
|
||||
@ -86,6 +88,7 @@ object BackendApp {
|
||||
shareImpl <- Resource.pure(
|
||||
OShare(store, itemSearchImpl, simpleSearchImpl, javaEmil)
|
||||
)
|
||||
notifyImpl <- ONotification(store, notificationMod)
|
||||
} yield new BackendApp[F] {
|
||||
val pubSub = pubSubT
|
||||
val login = loginImpl
|
||||
@ -110,5 +113,7 @@ object BackendApp {
|
||||
val clientSettings = clientSettingsImpl
|
||||
val totp = totpImpl
|
||||
val share = shareImpl
|
||||
val events = notificationMod
|
||||
val notification = notifyImpl
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,18 @@ import docspell.backend.signup.{Config => SignupConfig}
|
||||
import docspell.common._
|
||||
import docspell.store.JdbcConfig
|
||||
|
||||
import emil.javamail.Settings
|
||||
|
||||
case class Config(
|
||||
mailDebug: Boolean,
|
||||
jdbc: JdbcConfig,
|
||||
signup: SignupConfig,
|
||||
files: Config.Files
|
||||
) {}
|
||||
) {
|
||||
|
||||
def mailSettings: Settings =
|
||||
Settings.defaultSettings.copy(debug = mailDebug)
|
||||
}
|
||||
|
||||
object Config {
|
||||
|
||||
|
@ -9,10 +9,29 @@ package docspell.backend
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.MailAddressCodec
|
||||
import docspell.common._
|
||||
import docspell.notification.api.ChannelOrRef._
|
||||
import docspell.notification.api.PeriodicQueryArgs
|
||||
import docspell.store.records.RJob
|
||||
|
||||
object JobFactory {
|
||||
object JobFactory extends MailAddressCodec {
|
||||
def periodicQuery[F[_]: Sync](args: PeriodicQueryArgs, submitter: AccountId): F[RJob] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
now <- Timestamp.current[F]
|
||||
job = RJob.newJob(
|
||||
id,
|
||||
PeriodicQueryArgs.taskName,
|
||||
submitter.collective,
|
||||
args,
|
||||
s"Running periodic query, notify via ${args.channel.channelType}",
|
||||
now,
|
||||
submitter.user,
|
||||
Priority.Low,
|
||||
None
|
||||
)
|
||||
} yield job
|
||||
|
||||
def makePageCount[F[_]: Sync](
|
||||
args: MakePageCountArgs,
|
||||
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.backend
|
||||
|
||||
import emil._
|
||||
import emil.javamail.syntax._
|
||||
import io.circe.{Decoder, Encoder}
|
||||
|
||||
trait MailAddressCodec {
|
||||
|
||||
implicit val jsonEncoder: Encoder[MailAddress] =
|
||||
Encoder.encodeString.contramap(_.asUnicodeString)
|
||||
|
||||
implicit val jsonDecoder: Decoder[MailAddress] =
|
||||
Decoder.decodeString.emap(MailAddress.parse)
|
||||
}
|
||||
|
||||
object MailAddressCodec extends MailAddressCodec
|
@ -275,8 +275,8 @@ object Login {
|
||||
token <- RememberToken.user(rme.id, config.serverSecret)
|
||||
} yield token
|
||||
|
||||
private def check(given: String)(data: QLogin.Data): Boolean = {
|
||||
val passOk = BCrypt.checkpw(given, data.password.pass)
|
||||
private def check(givenPass: String)(data: QLogin.Data): Boolean = {
|
||||
val passOk = BCrypt.checkpw(givenPass, data.password.pass)
|
||||
checkNoPassword(data, Set(AccountSource.Local)) && passOk
|
||||
}
|
||||
|
||||
|
@ -10,19 +10,36 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common.{Duration, Ident, Priority}
|
||||
import docspell.notification.api.Event
|
||||
import docspell.notification.api.EventSink
|
||||
import docspell.pubsub.api.PubSubT
|
||||
import docspell.store.Store
|
||||
import docspell.store.queue.JobQueue
|
||||
import docspell.store.records.RJob
|
||||
|
||||
final class JobQueuePublish[F[_]: Sync](delegate: JobQueue[F], pubsub: PubSubT[F])
|
||||
extends JobQueue[F] {
|
||||
final class JobQueuePublish[F[_]: Sync](
|
||||
delegate: JobQueue[F],
|
||||
pubsub: PubSubT[F],
|
||||
eventSink: EventSink[F]
|
||||
) extends JobQueue[F] {
|
||||
|
||||
private def msg(job: RJob): JobSubmitted =
|
||||
JobSubmitted(job.id, job.group, job.task, job.args)
|
||||
|
||||
private def event(job: RJob): Event.JobSubmitted =
|
||||
Event.JobSubmitted(
|
||||
job.id,
|
||||
job.group,
|
||||
job.task,
|
||||
job.args,
|
||||
job.state,
|
||||
job.subject,
|
||||
job.submitter
|
||||
)
|
||||
|
||||
private def publish(job: RJob): F[Unit] =
|
||||
pubsub.publish1(JobSubmitted.topic, msg(job)).as(())
|
||||
pubsub.publish1(JobSubmitted.topic, msg(job)).as(()) *>
|
||||
eventSink.offer(event(job))
|
||||
|
||||
def insert(job: RJob) =
|
||||
delegate.insert(job).flatTap(_ => publish(job))
|
||||
@ -54,6 +71,10 @@ final class JobQueuePublish[F[_]: Sync](delegate: JobQueue[F], pubsub: PubSubT[F
|
||||
}
|
||||
|
||||
object JobQueuePublish {
|
||||
def apply[F[_]: Async](store: Store[F], pubSub: PubSubT[F]): Resource[F, JobQueue[F]] =
|
||||
JobQueue(store).map(q => new JobQueuePublish[F](q, pubSub))
|
||||
def apply[F[_]: Async](
|
||||
store: Store[F],
|
||||
pubSub: PubSubT[F],
|
||||
eventSink: EventSink[F]
|
||||
): Resource[F, JobQueue[F]] =
|
||||
JobQueue(store).map(q => new JobQueuePublish[F](q, pubSub, eventSink))
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import cats.data.{NonEmptyList => Nel}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.AttachedEvent
|
||||
import docspell.backend.ops.OCustomFields.CustomFieldData
|
||||
import docspell.backend.ops.OCustomFields.CustomFieldOrder
|
||||
import docspell.backend.ops.OCustomFields.FieldValue
|
||||
@ -20,6 +21,7 @@ import docspell.backend.ops.OCustomFields.RemoveValue
|
||||
import docspell.backend.ops.OCustomFields.SetValue
|
||||
import docspell.backend.ops.OCustomFields.SetValueResult
|
||||
import docspell.common._
|
||||
import docspell.notification.api.Event
|
||||
import docspell.store.AddResult
|
||||
import docspell.store.Store
|
||||
import docspell.store.UpdateResult
|
||||
@ -53,12 +55,15 @@ trait OCustomFields[F[_]] {
|
||||
def delete(coll: Ident, fieldIdOrName: Ident): F[UpdateResult]
|
||||
|
||||
/** Sets a value given a field an an item. Existing values are overwritten. */
|
||||
def setValue(item: Ident, value: SetValue): F[SetValueResult]
|
||||
def setValue(item: Ident, value: SetValue): F[AttachedEvent[SetValueResult]]
|
||||
|
||||
def setValueMultiple(items: Nel[Ident], value: SetValue): F[SetValueResult]
|
||||
def setValueMultiple(
|
||||
items: Nel[Ident],
|
||||
value: SetValue
|
||||
): F[AttachedEvent[SetValueResult]]
|
||||
|
||||
/** Deletes a value for a given field an item. */
|
||||
def deleteValue(in: RemoveValue): F[UpdateResult]
|
||||
def deleteValue(in: RemoveValue): F[AttachedEvent[UpdateResult]]
|
||||
|
||||
/** Finds all values to the given items */
|
||||
def findAllValues(itemIds: Nel[Ident]): F[List[FieldValue]]
|
||||
@ -196,13 +201,13 @@ object OCustomFields {
|
||||
UpdateResult.fromUpdate(store.transact(update.getOrElse(0)))
|
||||
}
|
||||
|
||||
def setValue(item: Ident, value: SetValue): F[SetValueResult] =
|
||||
def setValue(item: Ident, value: SetValue): F[AttachedEvent[SetValueResult]] =
|
||||
setValueMultiple(Nel.of(item), value)
|
||||
|
||||
def setValueMultiple(
|
||||
items: Nel[Ident],
|
||||
value: SetValue
|
||||
): F[SetValueResult] =
|
||||
): F[AttachedEvent[SetValueResult]] =
|
||||
(for {
|
||||
field <- EitherT.fromOptionF(
|
||||
store.transact(RCustomField.findByIdOrName(value.field, value.collective)),
|
||||
@ -224,17 +229,24 @@ object OCustomFields {
|
||||
.traverse(item => store.transact(RCustomField.setValue(field, item, fval)))
|
||||
.map(_.toList.sum)
|
||||
)
|
||||
} yield nu).fold(identity, _ => SetValueResult.success)
|
||||
mkEvent =
|
||||
Event.SetFieldValue.partial(items, field.id, fval)
|
||||
|
||||
def deleteValue(in: RemoveValue): F[UpdateResult] = {
|
||||
} yield AttachedEvent(SetValueResult.success)(mkEvent))
|
||||
.fold(AttachedEvent.only, identity)
|
||||
|
||||
def deleteValue(in: RemoveValue): F[AttachedEvent[UpdateResult]] = {
|
||||
val update =
|
||||
for {
|
||||
(for {
|
||||
field <- OptionT(RCustomField.findByIdOrName(in.field, in.collective))
|
||||
_ <- OptionT.liftF(logger.debug(s"Field found by '${in.field}': $field"))
|
||||
n <- OptionT.liftF(RCustomFieldValue.deleteValue(field.id, in.item))
|
||||
} yield n
|
||||
mkEvent = Event.DeleteFieldValue.partial(in.item, field.id)
|
||||
} yield AttachedEvent(n)(mkEvent))
|
||||
.getOrElse(AttachedEvent.only(0))
|
||||
.map(_.map(UpdateResult.fromUpdateRows))
|
||||
|
||||
UpdateResult.fromUpdate(store.transact(update.getOrElse(0)))
|
||||
store.transact(update)
|
||||
}
|
||||
|
||||
})
|
||||
|
@ -6,15 +6,17 @@
|
||||
|
||||
package docspell.backend.ops
|
||||
|
||||
import cats.data.{NonEmptyList, OptionT}
|
||||
import cats.data.{NonEmptyList => Nel, OptionT}
|
||||
import cats.effect.{Async, Resource}
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.AttachedEvent
|
||||
import docspell.backend.JobFactory
|
||||
import docspell.backend.fulltext.CreateIndex
|
||||
import docspell.backend.item.Merge
|
||||
import docspell.common._
|
||||
import docspell.ftsclient.FtsClient
|
||||
import docspell.notification.api.Event
|
||||
import docspell.store.queries.{QAttachment, QItem, QMoveAttachment}
|
||||
import docspell.store.queue.JobQueue
|
||||
import docspell.store.records._
|
||||
@ -26,42 +28,54 @@ import org.log4s.getLogger
|
||||
trait OItem[F[_]] {
|
||||
|
||||
/** Sets the given tags (removing all existing ones). */
|
||||
def setTags(item: Ident, tagIds: List[String], collective: Ident): F[UpdateResult]
|
||||
def setTags(
|
||||
item: Ident,
|
||||
tagIds: List[String],
|
||||
collective: Ident
|
||||
): F[AttachedEvent[UpdateResult]]
|
||||
|
||||
/** Sets tags for multiple items. The tags of the items will be replaced with the given
|
||||
* ones. Same as `setTags` but for multiple items.
|
||||
*/
|
||||
def setTagsMultipleItems(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
): F[AttachedEvent[UpdateResult]]
|
||||
|
||||
/** Create a new tag and add it to the item. */
|
||||
def addNewTag(item: Ident, tag: RTag): F[AddResult]
|
||||
def addNewTag(collective: Ident, item: Ident, tag: RTag): F[AttachedEvent[AddResult]]
|
||||
|
||||
/** Apply all tags to the given item. Tags must exist, but can be IDs or names. Existing
|
||||
* tags on the item are left unchanged.
|
||||
*/
|
||||
def linkTags(item: Ident, tags: List[String], collective: Ident): F[UpdateResult]
|
||||
def linkTags(
|
||||
item: Ident,
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[AttachedEvent[UpdateResult]]
|
||||
|
||||
def linkTagsMultipleItems(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
): F[AttachedEvent[UpdateResult]]
|
||||
|
||||
def removeTagsMultipleItems(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
): F[AttachedEvent[UpdateResult]]
|
||||
|
||||
/** Toggles tags of the given item. Tags must exist, but can be IDs or names. */
|
||||
def toggleTags(item: Ident, tags: List[String], collective: Ident): F[UpdateResult]
|
||||
def toggleTags(
|
||||
item: Ident,
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[AttachedEvent[UpdateResult]]
|
||||
|
||||
def setDirection(
|
||||
item: NonEmptyList[Ident],
|
||||
item: Nel[Ident],
|
||||
direction: Direction,
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
@ -69,13 +83,13 @@ trait OItem[F[_]] {
|
||||
def setFolder(item: Ident, folder: Option[Ident], collective: Ident): F[UpdateResult]
|
||||
|
||||
def setFolderMultiple(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
folder: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
|
||||
def setCorrOrg(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
org: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
@ -83,7 +97,7 @@ trait OItem[F[_]] {
|
||||
def addCorrOrg(item: Ident, org: OOrganization.OrgAndContacts): F[AddResult]
|
||||
|
||||
def setCorrPerson(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
person: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
@ -91,7 +105,7 @@ trait OItem[F[_]] {
|
||||
def addCorrPerson(item: Ident, person: OOrganization.PersonAndContacts): F[AddResult]
|
||||
|
||||
def setConcPerson(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
person: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
@ -99,7 +113,7 @@ trait OItem[F[_]] {
|
||||
def addConcPerson(item: Ident, person: OOrganization.PersonAndContacts): F[AddResult]
|
||||
|
||||
def setConcEquip(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
equip: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
@ -111,30 +125,30 @@ trait OItem[F[_]] {
|
||||
def setName(item: Ident, name: String, collective: Ident): F[UpdateResult]
|
||||
|
||||
def setNameMultiple(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
name: String,
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
|
||||
def setState(item: Ident, state: ItemState, collective: Ident): F[AddResult] =
|
||||
setStates(NonEmptyList.of(item), state, collective)
|
||||
setStates(Nel.of(item), state, collective)
|
||||
|
||||
def setStates(
|
||||
item: NonEmptyList[Ident],
|
||||
item: Nel[Ident],
|
||||
state: ItemState,
|
||||
collective: Ident
|
||||
): F[AddResult]
|
||||
|
||||
def restore(items: NonEmptyList[Ident], collective: Ident): F[UpdateResult]
|
||||
def restore(items: Nel[Ident], collective: Ident): F[UpdateResult]
|
||||
|
||||
def setItemDate(
|
||||
item: NonEmptyList[Ident],
|
||||
item: Nel[Ident],
|
||||
date: Option[Timestamp],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
|
||||
def setItemDueDate(
|
||||
item: NonEmptyList[Ident],
|
||||
item: Nel[Ident],
|
||||
date: Option[Timestamp],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
@ -143,14 +157,14 @@ trait OItem[F[_]] {
|
||||
|
||||
def deleteItem(itemId: Ident, collective: Ident): F[Int]
|
||||
|
||||
def deleteItemMultiple(items: NonEmptyList[Ident], collective: Ident): F[Int]
|
||||
def deleteItemMultiple(items: Nel[Ident], collective: Ident): F[Int]
|
||||
|
||||
def deleteAttachment(id: Ident, collective: Ident): F[Int]
|
||||
|
||||
def setDeletedState(items: NonEmptyList[Ident], collective: Ident): F[Int]
|
||||
def setDeletedState(items: Nel[Ident], collective: Ident): F[Int]
|
||||
|
||||
def deleteAttachmentMultiple(
|
||||
attachments: NonEmptyList[Ident],
|
||||
attachments: Nel[Ident],
|
||||
collective: Ident
|
||||
): F[Int]
|
||||
|
||||
@ -174,7 +188,7 @@ trait OItem[F[_]] {
|
||||
): F[UpdateResult]
|
||||
|
||||
def reprocessAll(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
account: AccountId,
|
||||
notifyJoex: Boolean
|
||||
): F[UpdateResult]
|
||||
@ -204,13 +218,12 @@ trait OItem[F[_]] {
|
||||
/** Merges a list of items into one item. The remaining items are deleted. */
|
||||
def merge(
|
||||
logger: Logger[F],
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult]
|
||||
}
|
||||
|
||||
object OItem {
|
||||
|
||||
def apply[F[_]: Async](
|
||||
store: Store[F],
|
||||
fts: FtsClient[F],
|
||||
@ -227,7 +240,7 @@ object OItem {
|
||||
|
||||
def merge(
|
||||
logger: Logger[F],
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
Merge(logger, store, this, createIndex).merge(items, collective).attempt.map {
|
||||
@ -250,52 +263,62 @@ object OItem {
|
||||
item: Ident,
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
linkTagsMultipleItems(NonEmptyList.of(item), tags, collective)
|
||||
): F[AttachedEvent[UpdateResult]] =
|
||||
linkTagsMultipleItems(Nel.of(item), tags, collective)
|
||||
|
||||
def linkTagsMultipleItems(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
): F[AttachedEvent[UpdateResult]] =
|
||||
tags.distinct match {
|
||||
case Nil => UpdateResult.success.pure[F]
|
||||
case Nil => AttachedEvent.only(UpdateResult.success).pure[F]
|
||||
case ws =>
|
||||
store.transact {
|
||||
(for {
|
||||
itemIds <- OptionT
|
||||
.liftF(RItem.filterItems(items, collective))
|
||||
.filter(_.nonEmpty)
|
||||
given <- OptionT.liftF(RTag.findAllByNameOrId(ws, collective))
|
||||
_ <- OptionT.liftF(
|
||||
itemIds.traverse(item =>
|
||||
RTagItem.appendTags(item, given.map(_.tagId).toList)
|
||||
store
|
||||
.transact {
|
||||
(for {
|
||||
itemIds <- OptionT
|
||||
.liftF(RItem.filterItems(items, collective))
|
||||
.subflatMap(l => Nel.fromFoldable(l))
|
||||
given <- OptionT.liftF(RTag.findAllByNameOrId(ws, collective))
|
||||
added <- OptionT.liftF(
|
||||
itemIds.traverse(item =>
|
||||
RTagItem.appendTags(item, given.map(_.tagId).toList)
|
||||
)
|
||||
)
|
||||
)
|
||||
} yield UpdateResult.success).getOrElse(UpdateResult.notFound)
|
||||
}
|
||||
ev = Event.TagsChanged.partial(
|
||||
itemIds,
|
||||
added.toList.flatten.map(_.id).toList,
|
||||
Nil
|
||||
)
|
||||
} yield AttachedEvent(UpdateResult.success)(ev))
|
||||
.getOrElse(AttachedEvent.only(UpdateResult.notFound))
|
||||
}
|
||||
}
|
||||
|
||||
def removeTagsMultipleItems(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
): F[AttachedEvent[UpdateResult]] =
|
||||
tags.distinct match {
|
||||
case Nil => UpdateResult.success.pure[F]
|
||||
case Nil => AttachedEvent.only(UpdateResult.success).pure[F]
|
||||
case ws =>
|
||||
store.transact {
|
||||
(for {
|
||||
itemIds <- OptionT
|
||||
.liftF(RItem.filterItems(items, collective))
|
||||
.filter(_.nonEmpty)
|
||||
.subflatMap(l => Nel.fromFoldable(l))
|
||||
given <- OptionT.liftF(RTag.findAllByNameOrId(ws, collective))
|
||||
_ <- OptionT.liftF(
|
||||
itemIds.traverse(item =>
|
||||
RTagItem.removeAllTags(item, given.map(_.tagId).toList)
|
||||
)
|
||||
)
|
||||
} yield UpdateResult.success).getOrElse(UpdateResult.notFound)
|
||||
mkEvent = Event.TagsChanged
|
||||
.partial(itemIds, Nil, given.map(_.tagId.id).toList)
|
||||
} yield AttachedEvent(UpdateResult.success)(mkEvent))
|
||||
.getOrElse(AttachedEvent.only(UpdateResult.notFound))
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,9 +326,9 @@ object OItem {
|
||||
item: Ident,
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
): F[AttachedEvent[UpdateResult]] =
|
||||
tags.distinct match {
|
||||
case Nil => UpdateResult.success.pure[F]
|
||||
case Nil => AttachedEvent.only(UpdateResult.success).pure[F]
|
||||
case kws =>
|
||||
val db =
|
||||
(for {
|
||||
@ -316,7 +339,14 @@ object OItem {
|
||||
toadd = given.map(_.tagId).diff(exist.map(_.tagId))
|
||||
_ <- OptionT.liftF(RTagItem.setAllTags(item, toadd))
|
||||
_ <- OptionT.liftF(RTagItem.removeAllTags(item, remove.toSeq))
|
||||
} yield UpdateResult.success).getOrElse(UpdateResult.notFound)
|
||||
mkEvent = Event.TagsChanged.partial(
|
||||
Nel.of(item),
|
||||
toadd.map(_.id).toList,
|
||||
remove.map(_.id).toList
|
||||
)
|
||||
|
||||
} yield AttachedEvent(UpdateResult.success)(mkEvent))
|
||||
.getOrElse(AttachedEvent.only(UpdateResult.notFound))
|
||||
|
||||
store.transact(db)
|
||||
}
|
||||
@ -325,41 +355,69 @@ object OItem {
|
||||
item: Ident,
|
||||
tagIds: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
setTagsMultipleItems(NonEmptyList.of(item), tagIds, collective)
|
||||
): F[AttachedEvent[UpdateResult]] =
|
||||
setTagsMultipleItems(Nel.of(item), tagIds, collective)
|
||||
|
||||
def setTagsMultipleItems(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
tags: List[String],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
UpdateResult.fromUpdate(store.transact(for {
|
||||
k <- RTagItem.deleteItemTags(items, collective)
|
||||
rtags <- RTag.findAllByNameOrId(tags, collective)
|
||||
res <- items.traverse(i => RTagItem.setAllTags(i, rtags.map(_.tagId)))
|
||||
n = res.fold
|
||||
} yield k + n))
|
||||
): F[AttachedEvent[UpdateResult]] = {
|
||||
val dbTask =
|
||||
for {
|
||||
k <- RTagItem.deleteItemTags(items, collective)
|
||||
given <- RTag.findAllByNameOrId(tags, collective)
|
||||
res <- items.traverse(i => RTagItem.setAllTags(i, given.map(_.tagId)))
|
||||
n = res.fold
|
||||
mkEvent = Event.TagsChanged.partial(
|
||||
items,
|
||||
given.map(_.tagId.id).toList,
|
||||
Nil
|
||||
)
|
||||
} yield AttachedEvent(k + n)(mkEvent)
|
||||
|
||||
def addNewTag(item: Ident, tag: RTag): F[AddResult] =
|
||||
for {
|
||||
data <- store.transact(dbTask)
|
||||
} yield data.map(UpdateResult.fromUpdateRows)
|
||||
}
|
||||
|
||||
def addNewTag(
|
||||
collective: Ident,
|
||||
item: Ident,
|
||||
tag: RTag
|
||||
): F[AttachedEvent[AddResult]] =
|
||||
(for {
|
||||
_ <- OptionT(store.transact(RItem.getCollective(item)))
|
||||
.filter(_ == tag.collective)
|
||||
addres <- OptionT.liftF(otag.add(tag))
|
||||
_ <- addres match {
|
||||
res <- addres match {
|
||||
case AddResult.Success =>
|
||||
OptionT.liftF(
|
||||
store.transact(RTagItem.setAllTags(item, List(tag.tagId)))
|
||||
store
|
||||
.transact(RTagItem.setAllTags(item, List(tag.tagId)))
|
||||
.map { _ =>
|
||||
AttachedEvent(())(
|
||||
Event.TagsChanged.partial(
|
||||
Nel.of(item),
|
||||
List(tag.tagId.id),
|
||||
Nil
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
case AddResult.EntityExists(_) =>
|
||||
OptionT.pure[F](0)
|
||||
OptionT.pure[F](AttachedEvent.only(()))
|
||||
case AddResult.Failure(_) =>
|
||||
OptionT.pure[F](0)
|
||||
OptionT.pure[F](AttachedEvent.only(()))
|
||||
}
|
||||
} yield addres)
|
||||
.getOrElse(AddResult.Failure(new Exception("Collective mismatch")))
|
||||
} yield res.map(_ => addres))
|
||||
.getOrElse(
|
||||
AttachedEvent.only(AddResult.Failure(new Exception("Collective mismatch")))
|
||||
)
|
||||
|
||||
def setDirection(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
direction: Direction,
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -383,7 +441,7 @@ object OItem {
|
||||
)
|
||||
|
||||
def setFolderMultiple(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
folder: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -404,7 +462,7 @@ object OItem {
|
||||
} yield res
|
||||
|
||||
def setCorrOrg(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
org: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -423,7 +481,7 @@ object OItem {
|
||||
OptionT.liftF(
|
||||
store.transact(
|
||||
RItem.updateCorrOrg(
|
||||
NonEmptyList.of(item),
|
||||
Nel.of(item),
|
||||
org.org.cid,
|
||||
Some(org.org.oid)
|
||||
)
|
||||
@ -438,7 +496,7 @@ object OItem {
|
||||
.getOrElse(AddResult.Failure(new Exception("Collective mismatch")))
|
||||
|
||||
def setCorrPerson(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
person: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -461,7 +519,7 @@ object OItem {
|
||||
store.transact(
|
||||
RItem
|
||||
.updateCorrPerson(
|
||||
NonEmptyList.of(item),
|
||||
Nel.of(item),
|
||||
person.person.cid,
|
||||
Some(person.person.pid)
|
||||
)
|
||||
@ -476,7 +534,7 @@ object OItem {
|
||||
.getOrElse(AddResult.Failure(new Exception("Collective mismatch")))
|
||||
|
||||
def setConcPerson(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
person: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -499,7 +557,7 @@ object OItem {
|
||||
store.transact(
|
||||
RItem
|
||||
.updateConcPerson(
|
||||
NonEmptyList.of(item),
|
||||
Nel.of(item),
|
||||
person.person.cid,
|
||||
Some(person.person.pid)
|
||||
)
|
||||
@ -514,7 +572,7 @@ object OItem {
|
||||
.getOrElse(AddResult.Failure(new Exception("Collective mismatch")))
|
||||
|
||||
def setConcEquip(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
equip: Option[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -533,7 +591,7 @@ object OItem {
|
||||
OptionT.liftF(
|
||||
store.transact(
|
||||
RItem
|
||||
.updateConcEquip(NonEmptyList.of(item), equip.cid, Some(equip.eid))
|
||||
.updateConcEquip(Nel.of(item), equip.cid, Some(equip.eid))
|
||||
)
|
||||
)
|
||||
case AddResult.EntityExists(_) =>
|
||||
@ -569,7 +627,7 @@ object OItem {
|
||||
)
|
||||
|
||||
def setNameMultiple(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
name: String,
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -590,7 +648,7 @@ object OItem {
|
||||
} yield res
|
||||
|
||||
def setStates(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
state: ItemState,
|
||||
collective: Ident
|
||||
): F[AddResult] =
|
||||
@ -600,7 +658,7 @@ object OItem {
|
||||
.map(AddResult.fromUpdate)
|
||||
|
||||
def restore(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
UpdateResult.fromUpdate(for {
|
||||
@ -612,7 +670,7 @@ object OItem {
|
||||
} yield n)
|
||||
|
||||
def setItemDate(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
date: Option[Timestamp],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -622,7 +680,7 @@ object OItem {
|
||||
)
|
||||
|
||||
def setItemDueDate(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
date: Option[Timestamp],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
@ -636,14 +694,14 @@ object OItem {
|
||||
.delete(store)(itemId, collective)
|
||||
.flatTap(_ => fts.removeItem(logger, itemId))
|
||||
|
||||
def deleteItemMultiple(items: NonEmptyList[Ident], collective: Ident): F[Int] =
|
||||
def deleteItemMultiple(items: Nel[Ident], collective: Ident): F[Int] =
|
||||
for {
|
||||
itemIds <- store.transact(RItem.filterItems(items, collective))
|
||||
results <- itemIds.traverse(item => deleteItem(item, collective))
|
||||
n = results.sum
|
||||
} yield n
|
||||
|
||||
def setDeletedState(items: NonEmptyList[Ident], collective: Ident): F[Int] =
|
||||
def setDeletedState(items: Nel[Ident], collective: Ident): F[Int] =
|
||||
for {
|
||||
n <- store.transact(RItem.setState(items, collective, ItemState.Deleted))
|
||||
_ <- items.traverse(id => fts.removeItem(logger, id))
|
||||
@ -658,7 +716,7 @@ object OItem {
|
||||
.flatTap(_ => fts.removeAttachment(logger, id))
|
||||
|
||||
def deleteAttachmentMultiple(
|
||||
attachments: NonEmptyList[Ident],
|
||||
attachments: Nel[Ident],
|
||||
collective: Ident
|
||||
): F[Int] =
|
||||
for {
|
||||
@ -710,7 +768,7 @@ object OItem {
|
||||
} yield UpdateResult.success).getOrElse(UpdateResult.notFound)
|
||||
|
||||
def reprocessAll(
|
||||
items: NonEmptyList[Ident],
|
||||
items: Nel[Ident],
|
||||
account: AccountId,
|
||||
notifyJoex: Boolean
|
||||
): F[UpdateResult] =
|
||||
|
@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.backend.ops
|
||||
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.data.{NonEmptyList => Nel}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.ops.ONotification.Hook
|
||||
import docspell.common._
|
||||
import docspell.jsonminiq.JsonMiniQuery
|
||||
import docspell.notification.api._
|
||||
import docspell.store.AddResult
|
||||
import docspell.store.Store
|
||||
import docspell.store.UpdateResult
|
||||
import docspell.store.queries.QNotification
|
||||
import docspell.store.records._
|
||||
|
||||
trait ONotification[F[_]] {
|
||||
|
||||
def sendMessage(
|
||||
logger: Logger[F],
|
||||
data: EventContext,
|
||||
channels: Seq[NotificationChannel]
|
||||
): F[Unit]
|
||||
|
||||
def offerEvents(ev: Iterable[Event]): F[Unit]
|
||||
|
||||
def mkNotificationChannel(channel: Channel): F[Vector[NotificationChannel]]
|
||||
|
||||
def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]]
|
||||
|
||||
def listChannels(account: AccountId): F[Vector[Channel]]
|
||||
|
||||
def deleteChannel(id: Ident, account: AccountId): F[UpdateResult]
|
||||
|
||||
def createChannel(channel: Channel, account: AccountId): F[AddResult]
|
||||
|
||||
def updateChannel(channel: Channel, account: AccountId): F[UpdateResult]
|
||||
|
||||
def listHooks(account: AccountId): F[Vector[Hook]]
|
||||
|
||||
def deleteHook(id: Ident, account: AccountId): F[UpdateResult]
|
||||
|
||||
def createHook(hook: Hook, account: AccountId): F[AddResult]
|
||||
|
||||
def updateHook(hook: Hook, account: AccountId): F[UpdateResult]
|
||||
|
||||
def sampleEvent(
|
||||
evt: EventType,
|
||||
account: AccountId,
|
||||
baseUrl: Option[LenientUri]
|
||||
): F[EventContext]
|
||||
|
||||
def sendSampleEvent(
|
||||
evt: EventType,
|
||||
channel: Channel,
|
||||
account: AccountId,
|
||||
baseUrl: Option[LenientUri]
|
||||
): F[ONotification.SendTestResult]
|
||||
}
|
||||
|
||||
object ONotification {
|
||||
private[this] val logger = org.log4s.getLogger
|
||||
|
||||
def apply[F[_]: Async](
|
||||
store: Store[F],
|
||||
notMod: NotificationModule[F]
|
||||
): Resource[F, ONotification[F]] =
|
||||
Resource.pure[F, ONotification[F]](new ONotification[F] {
|
||||
val log = Logger.log4s[F](logger)
|
||||
|
||||
def withUserId[A](
|
||||
account: AccountId
|
||||
)(f: Ident => F[UpdateResult]): F[UpdateResult] =
|
||||
OptionT(store.transact(RUser.findIdByAccount(account)))
|
||||
.semiflatMap(f)
|
||||
.getOrElse(UpdateResult.notFound)
|
||||
|
||||
def offerEvents(ev: Iterable[Event]): F[Unit] =
|
||||
ev.toList.traverse(notMod.offer(_)).as(())
|
||||
|
||||
def sendMessage(
|
||||
logger: Logger[F],
|
||||
data: EventContext,
|
||||
channels: Seq[NotificationChannel]
|
||||
): F[Unit] =
|
||||
notMod.send(logger, data, channels)
|
||||
|
||||
def sampleEvent(
|
||||
evt: EventType,
|
||||
account: AccountId,
|
||||
baseUrl: Option[LenientUri]
|
||||
): F[EventContext] =
|
||||
Event
|
||||
.sample[F](evt, account, baseUrl)
|
||||
.flatMap(notMod.sampleEvent.run)
|
||||
|
||||
def sendSampleEvent(
|
||||
evt: EventType,
|
||||
channel: Channel,
|
||||
account: AccountId,
|
||||
baseUrl: Option[LenientUri]
|
||||
): F[SendTestResult] =
|
||||
(for {
|
||||
ev <- sampleEvent(evt, account, baseUrl)
|
||||
logbuf <- Logger.buffer()
|
||||
ch <- mkNotificationChannel(channel)
|
||||
_ <- notMod.send(logbuf._2.andThen(log), ev, ch)
|
||||
logs <- logbuf._1.get
|
||||
res = SendTestResult(true, logs)
|
||||
} yield res).attempt
|
||||
.map {
|
||||
case Right(res) => res
|
||||
case Left(ex) =>
|
||||
val ps = new StringWriter()
|
||||
ex.printStackTrace(new PrintWriter(ps))
|
||||
SendTestResult(false, Vector(s"${ex.getMessage}\n$ps"))
|
||||
}
|
||||
|
||||
def listChannels(account: AccountId): F[Vector[Channel]] =
|
||||
store
|
||||
.transact(RNotificationChannel.getByAccount(account))
|
||||
.map(_.map(ChannelConv.makeChannel))
|
||||
|
||||
def deleteChannel(id: Ident, account: AccountId): F[UpdateResult] =
|
||||
UpdateResult
|
||||
.fromUpdate(
|
||||
store.transact(RNotificationChannel.deleteByAccount(id, account))
|
||||
)
|
||||
.flatTap(_ => log.info(s"Deleted channel ${id.id} for ${account.asString}"))
|
||||
|
||||
def createChannel(channel: Channel, account: AccountId): F[AddResult] =
|
||||
(for {
|
||||
newId <- OptionT.liftF(Ident.randomId[F])
|
||||
userId <- OptionT(store.transact(RUser.findIdByAccount(account)))
|
||||
r <- ChannelConv.makeRecord[F](store, Right(channel), newId, userId)
|
||||
_ <- OptionT.liftF(store.transact(RNotificationChannel.insert(r)))
|
||||
_ <- OptionT.liftF(log.debug(s"Created channel $r for $account"))
|
||||
} yield AddResult.Success)
|
||||
.getOrElse(AddResult.failure(new Exception("User not found!")))
|
||||
|
||||
def updateChannel(channel: Channel, account: AccountId): F[UpdateResult] =
|
||||
(for {
|
||||
userId <- OptionT(store.transact(RUser.findIdByAccount(account)))
|
||||
r <- ChannelConv.makeRecord[F](store, Right(channel), channel.id, userId)
|
||||
n <- OptionT.liftF(store.transact(RNotificationChannel.update(r)))
|
||||
} yield UpdateResult.fromUpdateRows(n)).getOrElse(UpdateResult.notFound)
|
||||
|
||||
def listHooks(account: AccountId): F[Vector[Hook]] =
|
||||
store.transact(for {
|
||||
list <- RNotificationHook.findAllByAccount(account)
|
||||
res <- list.traverse((Hook.fromRecord _).tupled)
|
||||
} yield res)
|
||||
|
||||
def deleteHook(id: Ident, account: AccountId): F[UpdateResult] =
|
||||
UpdateResult
|
||||
.fromUpdate(store.transact(RNotificationHook.deleteByAccount(id, account)))
|
||||
|
||||
def createHook(hook: Hook, account: AccountId): F[AddResult] =
|
||||
(for {
|
||||
_ <- OptionT.liftF(log.debug(s"Creating new notification hook: $hook"))
|
||||
channelId <- OptionT.liftF(Ident.randomId[F])
|
||||
userId <- OptionT(store.transact(RUser.findIdByAccount(account)))
|
||||
r <- ChannelConv.makeRecord[F](store, hook.channel, channelId, userId)
|
||||
_ <- OptionT.liftF(
|
||||
if (channelId == r.id) store.transact(RNotificationChannel.insert(r))
|
||||
else ().pure[F]
|
||||
)
|
||||
_ <- OptionT.liftF(log.debug(s"Created channel $r for $account"))
|
||||
hr <- OptionT.liftF(Hook.makeRecord(r, userId, hook))
|
||||
_ <- OptionT.liftF(store.transact(RNotificationHook.insert(hr)))
|
||||
_ <- OptionT.liftF(
|
||||
store.transact(RNotificationHookEvent.insertAll(hr.id, hook.events))
|
||||
)
|
||||
} yield AddResult.Success)
|
||||
.getOrElse(AddResult.failure(new Exception("User or channel not found!")))
|
||||
|
||||
def updateHook(hook: Hook, account: AccountId): F[UpdateResult] = {
|
||||
def withHook(f: RNotificationHook => F[UpdateResult]): F[UpdateResult] =
|
||||
withUserId(account)(userId =>
|
||||
OptionT(store.transact(RNotificationHook.getById(hook.id, userId)))
|
||||
.semiflatMap(f)
|
||||
.getOrElse(UpdateResult.notFound)
|
||||
)
|
||||
|
||||
def withChannel(
|
||||
r: RNotificationHook
|
||||
)(f: RNotificationChannel => F[UpdateResult]): F[UpdateResult] =
|
||||
ChannelConv
|
||||
.makeRecord(store, hook.channel, r.channelId, r.uid)
|
||||
.semiflatMap(f)
|
||||
.getOrElse(UpdateResult.notFound)
|
||||
|
||||
def doUpdate(r: RNotificationHook): F[UpdateResult] =
|
||||
withChannel(r) { ch =>
|
||||
UpdateResult.fromUpdate(store.transact(for {
|
||||
nc <- RNotificationChannel.update(ch)
|
||||
ne <- RNotificationHookEvent.updateAll(
|
||||
r.id,
|
||||
if (hook.allEvents) Nil else hook.events
|
||||
)
|
||||
nr <- RNotificationHook.update(
|
||||
r.copy(
|
||||
enabled = hook.enabled,
|
||||
allEvents = hook.allEvents,
|
||||
eventFilter = hook.eventFilter
|
||||
)
|
||||
)
|
||||
} yield nc + ne + nr))
|
||||
}
|
||||
|
||||
withHook(doUpdate)
|
||||
}
|
||||
|
||||
def mkNotificationChannel(channel: Channel): F[Vector[NotificationChannel]] =
|
||||
(for {
|
||||
rec <- ChannelConv
|
||||
.makeRecord(store, Right(channel), channel.id, Ident.unsafe(""))
|
||||
ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec)))
|
||||
} yield ch).getOrElse(Vector.empty)
|
||||
|
||||
def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]] =
|
||||
(for {
|
||||
rec <- OptionT(store.transact(RNotificationChannel.getByRef(ref)))
|
||||
ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec)))
|
||||
} yield ch).getOrElse(Vector.empty)
|
||||
})
|
||||
|
||||
object ChannelConv {
|
||||
|
||||
private[ops] def makeChannel(r: RNotificationChannel): Channel =
|
||||
r.fold(
|
||||
mail =>
|
||||
Channel.Mail(mail.id, mail.connection, Nel.fromListUnsafe(mail.recipients)),
|
||||
gotify => Channel.Gotify(r.id, gotify.url, gotify.appKey),
|
||||
matrix =>
|
||||
Channel.Matrix(r.id, matrix.homeServer, matrix.roomId, matrix.accessToken),
|
||||
http => Channel.Http(r.id, http.url)
|
||||
)
|
||||
|
||||
private[ops] def makeRecord[F[_]: Sync](
|
||||
store: Store[F],
|
||||
channelIn: Either[ChannelRef, Channel],
|
||||
id: Ident,
|
||||
userId: Ident
|
||||
): OptionT[F, RNotificationChannel] =
|
||||
channelIn match {
|
||||
case Left(ref) =>
|
||||
OptionT(store.transact(RNotificationChannel.getByRef(ref)))
|
||||
|
||||
case Right(channel) =>
|
||||
for {
|
||||
time <- OptionT.liftF(Timestamp.current[F])
|
||||
r <-
|
||||
channel match {
|
||||
case Channel.Mail(_, conn, recipients) =>
|
||||
for {
|
||||
mailConn <- OptionT(
|
||||
store.transact(RUserEmail.getByUser(userId, conn))
|
||||
)
|
||||
rec = RNotificationChannelMail(
|
||||
id,
|
||||
userId,
|
||||
mailConn.id,
|
||||
recipients.toList,
|
||||
time
|
||||
).vary
|
||||
} yield rec
|
||||
case Channel.Gotify(_, url, appKey) =>
|
||||
OptionT.pure[F](
|
||||
RNotificationChannelGotify(id, userId, url, appKey, time).vary
|
||||
)
|
||||
case Channel.Matrix(_, homeServer, roomId, accessToken) =>
|
||||
OptionT.pure[F](
|
||||
RNotificationChannelMatrix(
|
||||
id,
|
||||
userId,
|
||||
homeServer,
|
||||
roomId,
|
||||
accessToken,
|
||||
"m.text",
|
||||
time
|
||||
).vary
|
||||
)
|
||||
case Channel.Http(_, url) =>
|
||||
OptionT.pure[F](RNotificationChannelHttp(id, userId, url, time).vary)
|
||||
}
|
||||
} yield r
|
||||
}
|
||||
}
|
||||
|
||||
final case class Hook(
|
||||
id: Ident,
|
||||
enabled: Boolean,
|
||||
channel: Either[ChannelRef, Channel],
|
||||
allEvents: Boolean,
|
||||
eventFilter: Option[JsonMiniQuery],
|
||||
events: List[EventType]
|
||||
)
|
||||
|
||||
object Hook {
|
||||
import doobie._
|
||||
|
||||
private[ops] def fromRecord(
|
||||
r: RNotificationHook,
|
||||
events: List[EventType]
|
||||
): ConnectionIO[Hook] =
|
||||
RNotificationChannel
|
||||
.getByHook(r)
|
||||
.map(_.head)
|
||||
.map(ChannelConv.makeChannel)
|
||||
.map(ch => Hook(r.id, r.enabled, Right(ch), r.allEvents, r.eventFilter, events))
|
||||
|
||||
private[ops] def makeRecord[F[_]: Sync](
|
||||
ch: RNotificationChannel,
|
||||
userId: Ident,
|
||||
hook: Hook
|
||||
): F[RNotificationHook] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
time <- Timestamp.current[F]
|
||||
h = RNotificationHook(
|
||||
id,
|
||||
userId,
|
||||
hook.enabled,
|
||||
ch.fold(_.id.some, _ => None, _ => None, _ => None),
|
||||
ch.fold(_ => None, _.id.some, _ => None, _ => None),
|
||||
ch.fold(_ => None, _ => None, _.id.some, _ => None),
|
||||
ch.fold(_ => None, _ => None, _ => None, _.id.some),
|
||||
hook.allEvents,
|
||||
hook.eventFilter,
|
||||
time
|
||||
)
|
||||
} yield h
|
||||
}
|
||||
|
||||
final case class SendTestResult(success: Boolean, logMessages: Vector[String])
|
||||
}
|
@ -11,7 +11,10 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.backend.MailAddressCodec._
|
||||
import docspell.common._
|
||||
import docspell.notification.api.PeriodicDueItemsArgs
|
||||
import docspell.notification.api.PeriodicQueryArgs
|
||||
import docspell.store.queue.JobQueue
|
||||
import docspell.store.usertask._
|
||||
|
||||
@ -19,6 +22,22 @@ import io.circe.Encoder
|
||||
|
||||
trait OUserTask[F[_]] {
|
||||
|
||||
/** Return the settings for all periodic-query tasks of the given user */
|
||||
def getPeriodicQuery(scope: UserTaskScope): Stream[F, UserTask[PeriodicQueryArgs]]
|
||||
|
||||
/** Find a periodic-query task by the given id. */
|
||||
def findPeriodicQuery(
|
||||
id: Ident,
|
||||
scope: UserTaskScope
|
||||
): OptionT[F, UserTask[PeriodicQueryArgs]]
|
||||
|
||||
/** Updates the periodic-query task of the given user. */
|
||||
def submitPeriodicQuery(
|
||||
scope: UserTaskScope,
|
||||
subject: Option[String],
|
||||
task: UserTask[PeriodicQueryArgs]
|
||||
): F[Unit]
|
||||
|
||||
/** Return the settings for all scan-mailbox tasks of the current user. */
|
||||
def getScanMailbox(scope: UserTaskScope): Stream[F, UserTask[ScanMailboxArgs]]
|
||||
|
||||
@ -36,19 +55,19 @@ trait OUserTask[F[_]] {
|
||||
): F[Unit]
|
||||
|
||||
/** Return the settings for all the notify-due-items task of the current user. */
|
||||
def getNotifyDueItems(scope: UserTaskScope): Stream[F, UserTask[NotifyDueItemsArgs]]
|
||||
def getNotifyDueItems(scope: UserTaskScope): Stream[F, UserTask[PeriodicDueItemsArgs]]
|
||||
|
||||
/** Find a notify-due-items task by the given id. */
|
||||
def findNotifyDueItems(
|
||||
id: Ident,
|
||||
scope: UserTaskScope
|
||||
): OptionT[F, UserTask[NotifyDueItemsArgs]]
|
||||
): OptionT[F, UserTask[PeriodicDueItemsArgs]]
|
||||
|
||||
/** Updates the notify-due-items tasks and notifies the joex nodes. */
|
||||
def submitNotifyDueItems(
|
||||
scope: UserTaskScope,
|
||||
subject: Option[String],
|
||||
task: UserTask[NotifyDueItemsArgs]
|
||||
task: UserTask[PeriodicDueItemsArgs]
|
||||
): F[Unit]
|
||||
|
||||
/** Removes a user task with the given id. */
|
||||
@ -109,23 +128,42 @@ object OUserTask {
|
||||
|
||||
def getNotifyDueItems(
|
||||
scope: UserTaskScope
|
||||
): Stream[F, UserTask[NotifyDueItemsArgs]] =
|
||||
): Stream[F, UserTask[PeriodicDueItemsArgs]] =
|
||||
store
|
||||
.getByName[NotifyDueItemsArgs](scope, NotifyDueItemsArgs.taskName)
|
||||
.getByName[PeriodicDueItemsArgs](scope, PeriodicDueItemsArgs.taskName)
|
||||
|
||||
def findNotifyDueItems(
|
||||
id: Ident,
|
||||
scope: UserTaskScope
|
||||
): OptionT[F, UserTask[NotifyDueItemsArgs]] =
|
||||
): OptionT[F, UserTask[PeriodicDueItemsArgs]] =
|
||||
OptionT(getNotifyDueItems(scope).find(_.id == id).compile.last)
|
||||
|
||||
def submitNotifyDueItems(
|
||||
scope: UserTaskScope,
|
||||
subject: Option[String],
|
||||
task: UserTask[NotifyDueItemsArgs]
|
||||
task: UserTask[PeriodicDueItemsArgs]
|
||||
): F[Unit] =
|
||||
for {
|
||||
_ <- store.updateTask[NotifyDueItemsArgs](scope, subject, task)
|
||||
_ <- store.updateTask[PeriodicDueItemsArgs](scope, subject, task)
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
|
||||
def getPeriodicQuery(scope: UserTaskScope): Stream[F, UserTask[PeriodicQueryArgs]] =
|
||||
store.getByName[PeriodicQueryArgs](scope, PeriodicQueryArgs.taskName)
|
||||
|
||||
def findPeriodicQuery(
|
||||
id: Ident,
|
||||
scope: UserTaskScope
|
||||
): OptionT[F, UserTask[PeriodicQueryArgs]] =
|
||||
OptionT(getPeriodicQuery(scope).find(_.id == id).compile.last)
|
||||
|
||||
def submitPeriodicQuery(
|
||||
scope: UserTaskScope,
|
||||
subject: Option[String],
|
||||
task: UserTask[PeriodicQueryArgs]
|
||||
): F[Unit] =
|
||||
for {
|
||||
_ <- store.updateTask[PeriodicQueryArgs](scope, subject, task)
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
})
|
||||
|
Reference in New Issue
Block a user