mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 10:28:27 +00:00
Manage notification channels separately and migrate
It's more convenient to manage notification channels separately, as it is done with email settings. Notification hook and other forms are adopted to only select channels. Hooks can now use more than one channel.
This commit is contained in:
@ -11,7 +11,6 @@ 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
|
||||
|
||||
@ -25,7 +24,7 @@ object JobFactory extends MailAddressCodec {
|
||||
PeriodicQueryArgs.taskName,
|
||||
submitter.collective,
|
||||
args,
|
||||
s"Running periodic query, notify via ${args.channel.channelType}",
|
||||
s"Running periodic query, notify via ${args.channels.map(_.channelType)}",
|
||||
now,
|
||||
submitter.user,
|
||||
Priority.Low,
|
||||
|
@ -39,7 +39,10 @@ trait ONotification[F[_]] {
|
||||
userId: Ident
|
||||
): F[Vector[NotificationChannel]]
|
||||
|
||||
def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]]
|
||||
def findNotificationChannel(
|
||||
ref: ChannelRef,
|
||||
account: AccountId
|
||||
): F[Vector[NotificationChannel]]
|
||||
|
||||
def listChannels(account: AccountId): F[Vector[Channel]]
|
||||
|
||||
@ -65,7 +68,7 @@ trait ONotification[F[_]] {
|
||||
|
||||
def sendSampleEvent(
|
||||
evt: EventType,
|
||||
channel: Channel,
|
||||
channel: Nel[ChannelRef],
|
||||
account: AccountId,
|
||||
baseUrl: Option[LenientUri]
|
||||
): F[ONotification.SendTestResult]
|
||||
@ -89,7 +92,7 @@ object ONotification {
|
||||
.getOrElse(UpdateResult.notFound)
|
||||
|
||||
def offerEvents(ev: Iterable[Event]): F[Unit] =
|
||||
ev.toList.traverse(notMod.offer(_)).as(())
|
||||
ev.toList.traverse(notMod.offer).as(())
|
||||
|
||||
def sendMessage(
|
||||
logger: Logger[F],
|
||||
@ -109,33 +112,27 @@ object ONotification {
|
||||
|
||||
def sendSampleEvent(
|
||||
evt: EventType,
|
||||
channel: Channel,
|
||||
channels: Nel[ChannelRef],
|
||||
account: AccountId,
|
||||
baseUrl: Option[LenientUri]
|
||||
): F[SendTestResult] = {
|
||||
def doCreate(userId: Ident) =
|
||||
(for {
|
||||
ev <- sampleEvent(evt, account, baseUrl)
|
||||
logbuf <- Logger.buffer()
|
||||
ch <- mkNotificationChannel(channel, userId)
|
||||
_ <- 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"))
|
||||
}
|
||||
|
||||
OptionT(store.transact(RUser.findIdByAccount(account)))
|
||||
.semiflatMap(doCreate)
|
||||
.getOrElse(
|
||||
SendTestResult(false, Vector(s"No user found in db for: ${account.asString}"))
|
||||
): F[SendTestResult] =
|
||||
(for {
|
||||
ev <- sampleEvent(evt, account, baseUrl)
|
||||
logbuf <- Logger.buffer()
|
||||
ch <- channels.toList.toVector.flatTraverse(
|
||||
findNotificationChannel(_, account)
|
||||
)
|
||||
}
|
||||
_ <- 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
|
||||
@ -153,7 +150,7 @@ object ONotification {
|
||||
(for {
|
||||
newId <- OptionT.liftF(Ident.randomId[F])
|
||||
userId <- OptionT(store.transact(RUser.findIdByAccount(account)))
|
||||
r <- ChannelConv.makeRecord[F](store, log, Right(channel), newId, userId)
|
||||
r <- ChannelConv.makeRecord[F](store, channel, newId, userId)
|
||||
_ <- OptionT.liftF(store.transact(RNotificationChannel.insert(r)))
|
||||
_ <- OptionT.liftF(log.debug(s"Created channel $r for $account"))
|
||||
} yield AddResult.Success)
|
||||
@ -162,7 +159,7 @@ object ONotification {
|
||||
def updateChannel(channel: Channel, account: AccountId): F[UpdateResult] =
|
||||
(for {
|
||||
userId <- OptionT(store.transact(RUser.findIdByAccount(account)))
|
||||
r <- ChannelConv.makeRecord[F](store, log, Right(channel), channel.id, userId)
|
||||
r <- ChannelConv.makeRecord[F](store, channel, channel.id, userId)
|
||||
n <- OptionT.liftF(store.transact(RNotificationChannel.update(r)))
|
||||
} yield UpdateResult.fromUpdateRows(n)).getOrElse(UpdateResult.notFound)
|
||||
|
||||
@ -179,16 +176,14 @@ object ONotification {
|
||||
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, log, hook.channel, channelId, userId)
|
||||
hr <- OptionT.liftF(Hook.makeRecord(userId, hook))
|
||||
_ <- OptionT.liftF(
|
||||
if (channelId == r.id) store.transact(RNotificationChannel.insert(r))
|
||||
else ().pure[F]
|
||||
store.transact(
|
||||
RNotificationHook.insert(hr) *> RNotificationHookChannel
|
||||
.updateAll(hr.id, hook.channels.toList)
|
||||
)
|
||||
)
|
||||
_ <- 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))
|
||||
)
|
||||
@ -203,31 +198,25 @@ object ONotification {
|
||||
.getOrElse(UpdateResult.notFound)
|
||||
)
|
||||
|
||||
def withChannel(
|
||||
r: RNotificationHook
|
||||
)(f: RNotificationChannel => F[UpdateResult]): F[UpdateResult] =
|
||||
ChannelConv
|
||||
.makeRecord(store, log, 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
|
||||
UpdateResult.fromUpdate(store.transact(for {
|
||||
ne <- RNotificationHookEvent.updateAll(
|
||||
r.id,
|
||||
if (hook.allEvents) Nil else hook.events
|
||||
)
|
||||
nc <- RNotificationHookChannel.updateAll(
|
||||
r.id,
|
||||
hook.channels.toList
|
||||
)
|
||||
nr <- RNotificationHook.update(
|
||||
r.copy(
|
||||
enabled = hook.enabled,
|
||||
allEvents = hook.allEvents,
|
||||
eventFilter = hook.eventFilter
|
||||
)
|
||||
nr <- RNotificationHook.update(
|
||||
r.copy(
|
||||
enabled = hook.enabled,
|
||||
allEvents = hook.allEvents,
|
||||
eventFilter = hook.eventFilter
|
||||
)
|
||||
)
|
||||
} yield nc + ne + nr))
|
||||
}
|
||||
)
|
||||
|
||||
} yield nc + ne + nr))
|
||||
|
||||
withHook(doUpdate)
|
||||
}
|
||||
@ -238,13 +227,17 @@ object ONotification {
|
||||
): F[Vector[NotificationChannel]] =
|
||||
(for {
|
||||
rec <- ChannelConv
|
||||
.makeRecord(store, log, Right(channel), channel.id, userId)
|
||||
.makeRecord(store, channel, channel.id, userId)
|
||||
ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec)))
|
||||
} yield ch).getOrElse(Vector.empty)
|
||||
|
||||
def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]] =
|
||||
def findNotificationChannel(
|
||||
ref: ChannelRef,
|
||||
accountId: AccountId
|
||||
): F[Vector[NotificationChannel]] =
|
||||
(for {
|
||||
rec <- OptionT(store.transact(RNotificationChannel.getByRef(ref)))
|
||||
userId <- OptionT(store.transact(RUser.findIdByAccount(accountId)))
|
||||
rec <- OptionT(store.transact(RNotificationChannel.getByRef(ref, userId)))
|
||||
ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec)))
|
||||
} yield ch).getOrElse(Vector.empty)
|
||||
})
|
||||
@ -264,84 +257,30 @@ object ONotification {
|
||||
Channel.Gotify(r.id, gotify.name, gotify.url, gotify.appKey, gotify.priority),
|
||||
matrix =>
|
||||
Channel
|
||||
.Matrix(r.id, matrix.name, matrix.homeServer, matrix.roomId, matrix.accessToken),
|
||||
.Matrix(
|
||||
r.id,
|
||||
matrix.name,
|
||||
matrix.homeServer,
|
||||
matrix.roomId,
|
||||
matrix.accessToken
|
||||
),
|
||||
http => Channel.Http(r.id, http.name, http.url)
|
||||
)
|
||||
|
||||
private[ops] def makeRecord[F[_]: Sync](
|
||||
private[ops] def makeRecord[F[_]](
|
||||
store: Store[F],
|
||||
logger: Logger[F],
|
||||
channelIn: Either[ChannelRef, Channel],
|
||||
channel: Channel,
|
||||
id: Ident,
|
||||
userId: Ident
|
||||
): OptionT[F, RNotificationChannel] =
|
||||
channelIn match {
|
||||
case Left(ref) =>
|
||||
OptionT.liftF(logger.debug(s"Loading channel for ref: ${ref}")) *>
|
||||
OptionT(store.transact(RNotificationChannel.getByRef(ref)))
|
||||
RNotificationChannel.fromChannel(channel, id, userId).mapK(store.transform)
|
||||
|
||||
case Right(channel) =>
|
||||
for {
|
||||
time <- OptionT.liftF(Timestamp.current[F])
|
||||
r <-
|
||||
channel match {
|
||||
case Channel.Mail(_, name, conn, recipients) =>
|
||||
for {
|
||||
_ <- OptionT.liftF(
|
||||
logger.debug(
|
||||
s"Looking up user smtp for ${userId.id} and ${conn.id}"
|
||||
)
|
||||
)
|
||||
mailConn <- OptionT(
|
||||
store.transact(RUserEmail.getByUser(userId, conn))
|
||||
)
|
||||
rec = RNotificationChannelMail(
|
||||
id,
|
||||
userId,
|
||||
name,
|
||||
mailConn.id,
|
||||
recipients.toList,
|
||||
time
|
||||
).vary
|
||||
} yield rec
|
||||
case Channel.Gotify(_, name, url, appKey, prio) =>
|
||||
OptionT.pure[F](
|
||||
RNotificationChannelGotify(
|
||||
id,
|
||||
userId,
|
||||
name,
|
||||
url,
|
||||
appKey,
|
||||
prio,
|
||||
time
|
||||
).vary
|
||||
)
|
||||
case Channel.Matrix(_, name, homeServer, roomId, accessToken) =>
|
||||
OptionT.pure[F](
|
||||
RNotificationChannelMatrix(
|
||||
id,
|
||||
userId,
|
||||
name,
|
||||
homeServer,
|
||||
roomId,
|
||||
accessToken,
|
||||
"m.text",
|
||||
time
|
||||
).vary
|
||||
)
|
||||
case Channel.Http(_, name, url) =>
|
||||
OptionT.pure[F](
|
||||
RNotificationChannelHttp(id, userId, name, url, time).vary
|
||||
)
|
||||
}
|
||||
} yield r
|
||||
}
|
||||
}
|
||||
|
||||
final case class Hook(
|
||||
id: Ident,
|
||||
enabled: Boolean,
|
||||
channel: Either[ChannelRef, Channel],
|
||||
channels: List[ChannelRef],
|
||||
allEvents: Boolean,
|
||||
eventFilter: Option[JsonMiniQuery],
|
||||
events: List[EventType]
|
||||
@ -354,14 +293,12 @@ object ONotification {
|
||||
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))
|
||||
RNotificationHookChannel
|
||||
.allOfNel(r.id)
|
||||
.flatMap(rhcs => RNotificationHookChannel.resolveRefs(rhcs))
|
||||
.map(refs => Hook(r.id, r.enabled, refs, r.allEvents, r.eventFilter, events))
|
||||
|
||||
private[ops] def makeRecord[F[_]: Sync](
|
||||
ch: RNotificationChannel,
|
||||
userId: Ident,
|
||||
hook: Hook
|
||||
): F[RNotificationHook] =
|
||||
@ -372,10 +309,6 @@ object ONotification {
|
||||
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
|
||||
|
@ -11,7 +11,6 @@ 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
|
||||
|
Reference in New Issue
Block a user