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:
eikek
2022-01-19 21:51:18 +01:00
parent d41490dd88
commit 23cb34a6ff
78 changed files with 2583 additions and 1422 deletions

View File

@ -0,0 +1,33 @@
CREATE TABLE "notification_hook_channel" (
"id" varchar(254) not null primary key,
"hook_id" varchar(254) not null,
"channel_mail" varchar(254),
"channel_gotify" varchar(254),
"channel_matrix" varchar(254),
"channel_http" varchar(254),
foreign key ("hook_id") references "notification_hook"("id") on delete cascade,
foreign key ("channel_mail") references "notification_channel_mail"("id") on delete cascade,
foreign key ("channel_gotify") references "notification_channel_gotify"("id") on delete cascade,
foreign key ("channel_matrix") references "notification_channel_matrix"("id") on delete cascade,
foreign key ("channel_http") references "notification_channel_http"("id") on delete cascade,
unique("hook_id", "channel_mail"),
unique("hook_id", "channel_gotify"),
unique("hook_id", "channel_matrix"),
unique("hook_id", "channel_http")
);
insert into "notification_hook_channel" ("id", "hook_id", "channel_mail", "channel_gotify", "channel_matrix", "channel_http")
select random_uuid(), id, channel_mail, channel_gotify, channel_matrix, channel_http
from "notification_hook";
alter table "notification_hook"
drop column "channel_mail";
alter table "notification_hook"
drop column "channel_gotify";
alter table "notification_hook"
drop column "channel_matrix";
alter table "notification_hook"
drop column "channel_http";

View File

@ -0,0 +1,42 @@
CREATE TABLE `notification_hook_channel` (
`id` varchar(254) not null primary key,
`hook_id` varchar(254) not null,
`channel_mail` varchar(254),
`channel_gotify` varchar(254),
`channel_matrix` varchar(254),
`channel_http` varchar(254),
foreign key (`hook_id`) references `notification_hook`(`id`) on delete cascade,
foreign key (`channel_mail`) references `notification_channel_mail`(`id`) on delete cascade,
foreign key (`channel_gotify`) references `notification_channel_gotify`(`id`) on delete cascade,
foreign key (`channel_matrix`) references `notification_channel_matrix`(`id`) on delete cascade,
foreign key (`channel_http`) references `notification_channel_http`(`id`) on delete cascade,
unique(`hook_id`, `channel_mail`),
unique(`hook_id`, `channel_gotify`),
unique(`hook_id`, `channel_matrix`),
unique(`hook_id`, `channel_http`)
);
insert into `notification_hook_channel`
select md5(rand()), id, channel_mail, channel_gotify, channel_matrix, channel_http
from `notification_hook`;
alter table `notification_hook`
drop constraint `notification_hook_ibfk_2`;
alter table `notification_hook`
drop constraint `notification_hook_ibfk_3`;
alter table `notification_hook`
drop constraint `notification_hook_ibfk_4`;
alter table `notification_hook`
drop constraint `notification_hook_ibfk_5`;
alter table `notification_hook`
drop column `channel_mail`;
alter table `notification_hook`
drop column `channel_gotify`;
alter table `notification_hook`
drop column `channel_matrix`;
alter table `notification_hook`
drop column `channel_http`;

View File

@ -0,0 +1,33 @@
CREATE TABLE "notification_hook_channel" (
"id" varchar(254) not null primary key,
"hook_id" varchar(254) not null,
"channel_mail" varchar(254),
"channel_gotify" varchar(254),
"channel_matrix" varchar(254),
"channel_http" varchar(254),
foreign key ("hook_id") references "notification_hook"("id") on delete cascade,
foreign key ("channel_mail") references "notification_channel_mail"("id") on delete cascade,
foreign key ("channel_gotify") references "notification_channel_gotify"("id") on delete cascade,
foreign key ("channel_matrix") references "notification_channel_matrix"("id") on delete cascade,
foreign key ("channel_http") references "notification_channel_http"("id") on delete cascade,
unique("hook_id", "channel_mail"),
unique("hook_id", "channel_gotify"),
unique("hook_id", "channel_matrix"),
unique("hook_id", "channel_http")
);
insert into "notification_hook_channel" ("id", "hook_id", "channel_mail", "channel_gotify", "channel_matrix", "channel_http")
select md5(random()::text), id, channel_mail, channel_gotify, channel_matrix, channel_http
from "notification_hook";
alter table "notification_hook"
drop column "channel_mail";
alter table "notification_hook"
drop column "channel_gotify";
alter table "notification_hook"
drop column "channel_matrix";
alter table "notification_hook"
drop column "channel_http";

View File

@ -6,23 +6,23 @@
package db.migration
import cats.data.NonEmptyList
import cats.data.{NonEmptyList, OptionT}
import cats.effect.{IO, Sync}
import cats.implicits._
import docspell.common._
import docspell.common.syntax.StringSyntax._
import docspell.notification.api.Channel
import docspell.notification.api.PeriodicDueItemsArgs
import docspell.store.records.RPeriodicTask
import docspell.notification.api._
import docspell.store.records._
import db.migration.data.{PeriodicDueItemsArgsOld, PeriodicQueryArgsOld}
import doobie._
import doobie.implicits._
import doobie.util.transactor.Strategy
import emil.MailAddress
import emil.javamail.syntax._
import io.circe.Encoder
import io.circe.syntax._
import io.circe.{Decoder, Encoder}
import org.flywaydb.core.api.migration.Context
trait MigrationTasks {
@ -31,6 +31,8 @@ trait MigrationTasks {
implicit val jsonEncoder: Encoder[MailAddress] =
Encoder.encodeString.contramap(_.asUnicodeString)
implicit val jsonDecoder: Decoder[MailAddress] =
Decoder.decodeString.emap(MailAddress.parse)
def migrateDueItemTasks: ConnectionIO[Unit] =
for {
@ -42,20 +44,114 @@ trait MigrationTasks {
_ <- RPeriodicTask.setEnabledByTask(NotifyDueItemsArgs.taskName, false)
} yield ()
def migrateDueItemTask1(old: RPeriodicTask): ConnectionIO[Int] = {
val converted = old.args
.parseJsonAs[NotifyDueItemsArgs]
.leftMap(_.getMessage())
.flatMap(convertArgs)
def migratePeriodicItemTasks: ConnectionIO[Unit] =
for {
tasks2 <- RPeriodicTask.findByTask(PeriodicDueItemsArgsOld.taskName)
tasks3 <- RPeriodicTask.findByTask(PeriodicQueryArgsOld.taskName)
size = tasks2.size + tasks3.size
_ <- Sync[ConnectionIO].delay(
logger.info(s"Starting to migrate $size user tasks")
)
_ <- tasks2.traverse(migratePeriodicDueItemsTask)
_ <- tasks3.traverse(migratePeriodicQueryTask)
_ <- RPeriodicTask.setEnabledByTask(PeriodicQueryArgsOld.taskName, false)
_ <- RPeriodicTask.setEnabledByTask(PeriodicDueItemsArgsOld.taskName, false)
} yield ()
converted match {
case Right(args) =>
Sync[ConnectionIO].delay(logger.info(s"Converting user task: $old")) *>
private def migratePeriodicQueryTask(old: RPeriodicTask): ConnectionIO[Int] =
old.args
.parseJsonAs[PeriodicQueryArgsOld]
.leftMap { ex =>
logger.error(ex)(s"Error migrating tasks")
0.pure[ConnectionIO]
}
.map { oldArgs =>
val ref = oldArgs.channel match {
case Right(c) => saveChannel(c, oldArgs.account)
case Left(ref) => ref.pure[ConnectionIO]
}
ref.flatMap(channelRef =>
RPeriodicTask.updateTask(
old.id,
PeriodicQueryArgs.taskName,
PeriodicQueryArgs(
oldArgs.account,
NonEmptyList.of(channelRef),
oldArgs.query,
oldArgs.bookmark,
oldArgs.baseUrl,
oldArgs.contentStart
).asJson.noSpaces
)
)
}
.fold(identity, identity)
private def migratePeriodicDueItemsTask(old: RPeriodicTask): ConnectionIO[Int] =
old.args
.parseJsonAs[PeriodicDueItemsArgsOld]
.leftMap { ex =>
logger.error(ex)(s"Error migrating tasks")
0.pure[ConnectionIO]
}
.map { oldArgs =>
val ref = oldArgs.channel match {
case Right(c) => saveChannel(c, oldArgs.account)
case Left(ref) => ref.pure[ConnectionIO]
}
ref.flatMap(channelRef =>
RPeriodicTask.updateTask(
old.id,
PeriodicDueItemsArgs.taskName,
args.asJson.noSpaces
PeriodicDueItemsArgs(
oldArgs.account,
NonEmptyList.of(channelRef),
oldArgs.remindDays,
oldArgs.daysBack,
oldArgs.tagsInclude,
oldArgs.tagsExclude,
oldArgs.baseUrl
).asJson.noSpaces
)
)
}
.fold(identity, identity)
private def saveChannel(ch: Channel, account: AccountId): ConnectionIO[ChannelRef] =
(for {
newId <- OptionT.liftF(Ident.randomId[ConnectionIO])
userId <- OptionT(RUser.findIdByAccount(account))
r <- RNotificationChannel.fromChannel(ch, newId, userId)
_ <- OptionT.liftF(RNotificationChannel.insert(r))
_ <- OptionT.liftF(
Sync[ConnectionIO].delay(logger.debug(s"Created channel $r for $account"))
)
ref = r.asRef
} yield ref)
.getOrElseF(Sync[ConnectionIO].raiseError(new Exception("User not found!")))
private def migrateDueItemTask1(old: RPeriodicTask): ConnectionIO[Int] = {
val converted = old.args
.parseJsonAs[NotifyDueItemsArgs]
.leftMap(_.getMessage())
.map(convertArgs)
converted match {
case Right(args) =>
val task = args
.semiflatMap(a =>
RPeriodicTask
.updateTask(
old.id,
PeriodicDueItemsArgs.taskName,
a.asJson.noSpaces
)
)
.getOrElse(0)
Sync[ConnectionIO].delay(logger.info(s"Converting user task: $old")) *> task
case Left(err) =>
logger.error(s"Error converting user task: $old. $err")
@ -63,22 +159,44 @@ trait MigrationTasks {
}
}
def convertArgs(old: NotifyDueItemsArgs): Either[String, PeriodicDueItemsArgs] =
old.recipients
.traverse(MailAddress.parse)
.flatMap(l => NonEmptyList.fromList(l).toRight("No recipients provided"))
.map { rec =>
PeriodicDueItemsArgs(
old.account,
Right(Channel.Mail(Ident.unsafe(""), None, old.smtpConnection, rec)),
old.remindDays,
old.daysBack,
old.tagsInclude,
old.tagsExclude,
old.itemDetailUrl
)
private def convertArgs(
old: NotifyDueItemsArgs
): OptionT[ConnectionIO, PeriodicDueItemsArgs] = {
val recs = old.recipients
.map(MailAddress.parse)
.flatMap {
case Right(m) => Some(m)
case Left(err) =>
logger.warn(s"Cannot read mail address: $err. Skip this while migrating.")
None
}
for {
userId <- OptionT(RUser.findIdByAccount(old.account))
id <- OptionT.liftF(Ident.randomId[ConnectionIO])
now <- OptionT.liftF(Timestamp.current[ConnectionIO])
chName = Some("migrate notify items")
ch = RNotificationChannelMail(
id,
userId,
chName,
old.smtpConnection,
recs,
now
)
_ <- OptionT.liftF(RNotificationChannelMail.insert(ch))
args = PeriodicDueItemsArgs(
old.account,
NonEmptyList.of(ChannelRef(ch.id, ChannelType.Mail, chName)),
old.remindDays,
old.daysBack,
old.tagsInclude,
old.tagsExclude,
old.itemDetailUrl
)
} yield args
}
def mkTransactor(ctx: Context): Transactor[IO] = {
val xa = Transactor.fromConnection[IO](ctx.getConnection())
Transactor.strategy.set(xa, Strategy.void) // transactions are handled by flyway

View File

@ -0,0 +1,48 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package db.migration.data
import docspell.common._
import emil.MailAddress
import io.circe.generic.semiauto
import io.circe.{Decoder, Encoder}
/** Arguments to the notification task.
*
* This tasks queries items with a due date and informs the user via mail.
*
* If the structure changes, there must be some database migration to update or remove
* the json data of the corresponding task.
*/
final case class PeriodicDueItemsArgsOld(
account: AccountId,
channel: ChannelOrRef,
remindDays: Int,
daysBack: Option[Int],
tagsInclude: List[Ident],
tagsExclude: List[Ident],
baseUrl: Option[LenientUri]
)
object PeriodicDueItemsArgsOld {
val taskName = Ident.unsafe("periodic-due-items-notify")
implicit def jsonDecoder(implicit
mc: Decoder[MailAddress]
): Decoder[PeriodicDueItemsArgsOld] = {
implicit val x = ChannelOrRef.jsonDecoder
semiauto.deriveDecoder
}
implicit def jsonEncoder(implicit
mc: Encoder[MailAddress]
): Encoder[PeriodicDueItemsArgsOld] = {
implicit val x = ChannelOrRef.jsonEncoder
semiauto.deriveEncoder
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package db.migration.data
import docspell.common._
import emil.MailAddress
import io.circe.generic.semiauto
import io.circe.{Decoder, Encoder}
final case class PeriodicQueryArgsOld(
account: AccountId,
channel: ChannelOrRef,
query: Option[ItemQueryString],
bookmark: Option[String],
baseUrl: Option[LenientUri],
contentStart: Option[String]
)
object PeriodicQueryArgsOld {
val taskName = Ident.unsafe("periodic-query-notify")
implicit def jsonDecoder(implicit
mc: Decoder[MailAddress]
): Decoder[PeriodicQueryArgsOld] = {
implicit val x = ChannelOrRef.jsonDecoder
semiauto.deriveDecoder
}
implicit def jsonEncoder(implicit
mc: Encoder[MailAddress]
): Encoder[PeriodicQueryArgsOld] = {
implicit val x = ChannelOrRef.jsonEncoder
semiauto.deriveEncoder
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package db.migration
import docspell.notification.api._
import emil.MailAddress
import io.circe.{Decoder, Encoder}
package object data {
type ChannelOrRef = Either[ChannelRef, Channel]
object ChannelOrRef {
implicit def jsonDecoder(implicit mc: Decoder[MailAddress]): Decoder[ChannelOrRef] =
Channel.jsonDecoder.either(ChannelRef.jsonDecoder).map(_.swap)
implicit def jsonEncoder(implicit mc: Encoder[MailAddress]): Encoder[ChannelOrRef] =
Encoder.instance(_.fold(ChannelRef.jsonEncoder.apply, Channel.jsonEncoder.apply))
implicit class ChannelOrRefOpts(cr: ChannelOrRef) {
def channelType: ChannelType =
cr.fold(_.channelType, _.channelType)
}
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package db.migration.h2
import cats.effect.unsafe.implicits._
import db.migration.MigrationTasks
import doobie.implicits._
import org.flywaydb.core.api.migration.{BaseJavaMigration, Context}
class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks {
val logger = org.log4s.getLogger
override def migrate(ctx: Context): Unit = {
val xa = mkTransactor(ctx)
migratePeriodicItemTasks.transact(xa).unsafeRunSync()
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package db.migration.mariadb
import cats.effect.unsafe.implicits._
import db.migration.MigrationTasks
import doobie.implicits._
import org.flywaydb.core.api.migration.{BaseJavaMigration, Context}
class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks {
val logger = org.log4s.getLogger
override def migrate(ctx: Context): Unit = {
val xa = mkTransactor(ctx)
migratePeriodicItemTasks.transact(xa).unsafeRunSync()
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package db.migration.postgresql
import cats.effect.unsafe.implicits._
import db.migration.MigrationTasks
import doobie.implicits._
import org.flywaydb.core.api.migration.{BaseJavaMigration, Context}
class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks {
val logger = org.log4s.getLogger
override def migrate(ctx: Context): Unit = {
val xa = mkTransactor(ctx)
migratePeriodicItemTasks.transact(xa).unsafeRunSync()
}
}

View File

@ -12,7 +12,7 @@ import java.time.{Instant, LocalDate}
import docspell.common._
import docspell.common.syntax.all._
import docspell.jsonminiq.JsonMiniQuery
import docspell.notification.api.EventType
import docspell.notification.api.{ChannelType, EventType}
import docspell.query.{ItemQuery, ItemQueryParser}
import docspell.totp.Key
@ -156,6 +156,9 @@ trait DoobieMeta extends EmilDoobieMeta {
implicit val metaJsonMiniQuery: Meta[JsonMiniQuery] =
Meta[String].timap(JsonMiniQuery.unsafeParse)(_.unsafeAsString)
implicit val channelTypeRead: Read[ChannelType] =
Read[String].map(ChannelType.unsafeFromString)
}
object DoobieMeta extends DoobieMeta {

View File

@ -27,7 +27,11 @@ object QNotification {
def findChannelsForEvent(event: Event): ConnectionIO[Vector[HookChannel]] =
for {
hooks <- listHooks(event.account.collective, event.eventType)
chs <- hooks.traverse(readHookChannel)
chs <- hooks.traverse(h =>
listChannels(h.id)
.flatMap(_.flatTraverse(hc => readHookChannel(h.uid, hc)))
.map(c => HookChannel(h, c))
)
} yield chs
// --
@ -50,21 +54,27 @@ object QNotification {
)
).query[RNotificationHook].to[Vector]
def listChannels(hookId: Ident): ConnectionIO[Vector[RNotificationHookChannel]] =
RNotificationHookChannel.allOf(hookId)
def readHookChannel(
hook: RNotificationHook
): ConnectionIO[HookChannel] =
userId: Ident,
hook: RNotificationHookChannel
): ConnectionIO[Vector[NotificationChannel]] =
for {
c1 <- read(hook.channelMail)(RNotificationChannelMail.getById)(
c1 <- read(hook.channelMail)(RNotificationChannelMail.getById(userId))(
ChannelMap.readMail
)
c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById)(
c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById(userId))(
ChannelMap.readGotify
)
c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById)(
c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById(userId))(
ChannelMap.readMatrix
)
c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById)(ChannelMap.readHttp)
} yield HookChannel(hook, c1 ++ c2 ++ c3 ++ c4)
c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById(userId))(
ChannelMap.readHttp
)
} yield c1 ++ c2 ++ c3 ++ c4
def readChannel(ch: RNotificationChannel): ConnectionIO[Vector[NotificationChannel]] =
ch.fold(

View File

@ -7,10 +7,10 @@
package docspell.store.records
import cats.data.OptionT
import cats.implicits._
import docspell.common._
import docspell.notification.api.ChannelRef
import docspell.notification.api.ChannelType
import docspell.notification.api.{Channel, ChannelRef, ChannelType}
import doobie._
@ -20,6 +20,17 @@ sealed trait RNotificationChannel {
def name: Option[String] = fold(_.name, _.name, _.name, _.name)
def channelType: ChannelType =
fold(
_ => ChannelType.Mail,
_ => ChannelType.Gotify,
_ => ChannelType.Matrix,
_ => ChannelType.Http
)
def asRef: ChannelRef =
ChannelRef(id, channelType, name)
def fold[A](
f1: RNotificationChannelMail => A,
f2: RNotificationChannelGotify => A,
@ -93,42 +104,60 @@ object RNotificationChannel {
Matrix.apply
) ++ http.map(Http.apply)
def getById(id: Ident): ConnectionIO[Vector[RNotificationChannel]] =
def getById(id: Ident, userId: Ident): ConnectionIO[Vector[RNotificationChannel]] =
for {
mail <- RNotificationChannelMail.getById(id)
gotify <- RNotificationChannelGotify.getById(id)
matrix <- RNotificationChannelMatrix.getById(id)
http <- RNotificationChannelHttp.getById(id)
mail <- RNotificationChannelMail.getById(userId)(id)
gotify <- RNotificationChannelGotify.getById(userId)(id)
matrix <- RNotificationChannelMatrix.getById(userId)(id)
http <- RNotificationChannelHttp.getById(userId)(id)
} yield mail.map(Email.apply).toVector ++
gotify.map(Gotify.apply).toVector ++
matrix.map(Matrix.apply).toVector ++
http.map(Http.apply).toVector
def getByRef(ref: ChannelRef): ConnectionIO[Option[RNotificationChannel]] =
def getByRef(
ref: ChannelRef,
userId: Ident
): ConnectionIO[Option[RNotificationChannel]] =
ref.channelType match {
case ChannelType.Mail =>
RNotificationChannelMail.getById(ref.id).map(_.map(Email.apply))
RNotificationChannelMail.getById(userId)(ref.id).map(_.map(Email.apply))
case ChannelType.Matrix =>
RNotificationChannelMatrix.getById(ref.id).map(_.map(Matrix.apply))
RNotificationChannelMatrix.getById(userId)(ref.id).map(_.map(Matrix.apply))
case ChannelType.Gotify =>
RNotificationChannelGotify.getById(ref.id).map(_.map(Gotify.apply))
RNotificationChannelGotify.getById(userId)(ref.id).map(_.map(Gotify.apply))
case ChannelType.Http =>
RNotificationChannelHttp.getById(ref.id).map(_.map(Http.apply))
RNotificationChannelHttp.getById(userId)(ref.id).map(_.map(Http.apply))
}
def getByHook(r: RNotificationHook): ConnectionIO[Vector[RNotificationChannel]] = {
def getByHook(hook: RNotificationHook): ConnectionIO[Vector[RNotificationChannel]] = {
def opt(id: Option[Ident]): OptionT[ConnectionIO, Ident] =
OptionT.fromOption(id)
for {
mail <- opt(r.channelMail).flatMapF(RNotificationChannelMail.getById).value
gotify <- opt(r.channelGotify).flatMapF(RNotificationChannelGotify.getById).value
matrix <- opt(r.channelMatrix).flatMapF(RNotificationChannelMatrix.getById).value
http <- opt(r.channelHttp).flatMapF(RNotificationChannelHttp.getById).value
} yield mail.map(Email.apply).toVector ++
gotify.map(Gotify.apply).toVector ++
matrix.map(Matrix.apply).toVector ++
http.map(Http.apply).toVector
def find(
r: RNotificationHookChannel
): ConnectionIO[Vector[RNotificationChannel]] =
for {
mail <- opt(r.channelMail)
.flatMapF(RNotificationChannelMail.getById(hook.uid))
.value
gotify <- opt(r.channelGotify)
.flatMapF(RNotificationChannelGotify.getById(hook.uid))
.value
matrix <- opt(r.channelMatrix)
.flatMapF(RNotificationChannelMatrix.getById(hook.uid))
.value
http <- opt(r.channelHttp)
.flatMapF(RNotificationChannelHttp.getById(hook.uid))
.value
} yield mail.map(Email.apply).toVector ++
gotify.map(Gotify.apply).toVector ++
matrix.map(Matrix.apply).toVector ++
http.map(Http.apply).toVector
RNotificationHookChannel
.allOf(hook.id)
.flatMap(_.flatTraverse(find))
}
def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] =
@ -138,4 +167,63 @@ object RNotificationChannel {
n3 <- RNotificationChannelMatrix.deleteByAccount(id, account)
n4 <- RNotificationChannelHttp.deleteByAccount(id, account)
} yield n1 + n2 + n3 + n4
def fromChannel(
channel: Channel,
id: Ident,
userId: Ident
): OptionT[ConnectionIO, RNotificationChannel] =
for {
time <- OptionT.liftF(Timestamp.current[ConnectionIO])
logger = Logger.log4s[ConnectionIO](org.log4s.getLogger)
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(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[ConnectionIO](
RNotificationChannelGotify(
id,
userId,
name,
url,
appKey,
prio,
time
).vary
)
case Channel.Matrix(_, name, homeServer, roomId, accessToken) =>
OptionT.pure[ConnectionIO](
RNotificationChannelMatrix(
id,
userId,
name,
homeServer,
roomId,
accessToken,
"m.text",
time
).vary
)
case Channel.Http(_, name, url) =>
OptionT.pure[ConnectionIO](
RNotificationChannelHttp(id, userId, name, url, time).vary
)
}
} yield r
}

View File

@ -49,8 +49,12 @@ object RNotificationChannelGotify {
def as(alias: String): Table =
Table(Some(alias))
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelGotify]] =
run(select(T.all), from(T), T.id === id).query[RNotificationChannelGotify].option
def getById(
userId: Ident
)(id: Ident): ConnectionIO[Option[RNotificationChannelGotify]] =
run(select(T.all), from(T), T.id === id && T.uid === userId)
.query[RNotificationChannelGotify]
.option
def insert(r: RNotificationChannelGotify): ConnectionIO[Int] =
DML.insert(

View File

@ -45,8 +45,10 @@ object RNotificationChannelHttp {
def as(alias: String): Table =
Table(Some(alias))
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelHttp]] =
run(select(T.all), from(T), T.id === id).query[RNotificationChannelHttp].option
def getById(userId: Ident)(id: Ident): ConnectionIO[Option[RNotificationChannelHttp]] =
run(select(T.all), from(T), T.id === id && T.uid === userId)
.query[RNotificationChannelHttp]
.option
def insert(r: RNotificationChannelHttp): ConnectionIO[Int] =
DML.insert(T, T.all, sql"${r.id},${r.uid},${r.name},${r.url},${r.created}")

View File

@ -65,8 +65,10 @@ object RNotificationChannelMail {
)
)
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] =
run(select(T.all), from(T), T.id === id).query[RNotificationChannelMail].option
def getById(userId: Ident)(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] =
run(select(T.all), from(T), T.id === id && T.uid === userId)
.query[RNotificationChannelMail]
.option
def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelMail]] = {
val user = RUser.as("u")

View File

@ -77,8 +77,12 @@ object RNotificationChannelMatrix {
)
)
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMatrix]] =
run(select(T.all), from(T), T.id === id).query[RNotificationChannelMatrix].option
def getById(userId: Ident)(
id: Ident
): ConnectionIO[Option[RNotificationChannelMatrix]] =
run(select(T.all), from(T), T.id === id && T.uid === userId)
.query[RNotificationChannelMatrix]
.option
def getByAccount(
account: AccountId

View File

@ -7,7 +7,6 @@
package docspell.store.records
import cats.data.NonEmptyList
import cats.implicits._
import docspell.common._
import docspell.jsonminiq.JsonMiniQuery
@ -22,115 +21,18 @@ final case class RNotificationHook(
id: Ident,
uid: Ident,
enabled: Boolean,
channelMail: Option[Ident],
channelGotify: Option[Ident],
channelMatrix: Option[Ident],
channelHttp: Option[Ident],
allEvents: Boolean,
eventFilter: Option[JsonMiniQuery],
created: Timestamp
) {
def channelId: Ident =
channelMail
.orElse(channelGotify)
.orElse(channelMatrix)
.orElse(channelHttp)
.getOrElse(
sys.error(s"Illegal internal state: notification hook has no channel: ${id.id}")
)
}
) {}
object RNotificationHook {
def mail(
id: Ident,
uid: Ident,
enabled: Boolean,
channelMail: Ident,
created: Timestamp
): RNotificationHook =
RNotificationHook(
id,
uid,
enabled,
channelMail.some,
None,
None,
None,
false,
None,
created
)
def gotify(
id: Ident,
uid: Ident,
enabled: Boolean,
channelGotify: Ident,
created: Timestamp
): RNotificationHook =
RNotificationHook(
id,
uid,
enabled,
None,
channelGotify.some,
None,
None,
false,
None,
created
)
def matrix(
id: Ident,
uid: Ident,
enabled: Boolean,
channelMatrix: Ident,
created: Timestamp
): RNotificationHook =
RNotificationHook(
id,
uid,
enabled,
None,
None,
channelMatrix.some,
None,
false,
None,
created
)
def http(
id: Ident,
uid: Ident,
enabled: Boolean,
channelHttp: Ident,
created: Timestamp
): RNotificationHook =
RNotificationHook(
id,
uid,
enabled,
None,
None,
None,
channelHttp.some,
false,
None,
created
)
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "notification_hook"
val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this)
val enabled = Column[Boolean]("enabled", this)
val channelMail = Column[Ident]("channel_mail", this)
val channelGotify = Column[Ident]("channel_gotify", this)
val channelMatrix = Column[Ident]("channel_matrix", this)
val channelHttp = Column[Ident]("channel_http", this)
val allEvents = Column[Boolean]("all_events", this)
val eventFilter = Column[JsonMiniQuery]("event_filter", this)
val created = Column[Timestamp]("created", this)
@ -140,10 +42,6 @@ object RNotificationHook {
id,
uid,
enabled,
channelMail,
channelGotify,
channelMatrix,
channelHttp,
allEvents,
eventFilter,
created
@ -157,7 +55,7 @@ object RNotificationHook {
DML.insert(
T,
T.all,
sql"${r.id},${r.uid},${r.enabled},${r.channelMail},${r.channelGotify},${r.channelMatrix},${r.channelHttp},${r.allEvents},${r.eventFilter},${r.created}"
sql"${r.id},${r.uid},${r.enabled},${r.allEvents},${r.eventFilter},${r.created}"
)
def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = {
@ -174,10 +72,6 @@ object RNotificationHook {
T.id === r.id && T.uid === r.uid,
DML.set(
T.enabled.setTo(r.enabled),
T.channelMail.setTo(r.channelMail),
T.channelGotify.setTo(r.channelGotify),
T.channelMatrix.setTo(r.channelMatrix),
T.channelHttp.setTo(r.channelHttp),
T.allEvents.setTo(r.allEvents),
T.eventFilter.setTo(r.eventFilter)
)

View File

@ -0,0 +1,236 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.{NonEmptyList => Nel}
import cats.effect.Sync
import cats.implicits._
import docspell.common._
import docspell.notification.api.{ChannelRef, ChannelType}
import docspell.store.qb.DSL._
import docspell.store.qb._
import doobie._
import doobie.implicits._
final case class RNotificationHookChannel(
id: Ident,
hookId: Ident,
channelMail: Option[Ident],
channelGotify: Option[Ident],
channelMatrix: Option[Ident],
channelHttp: Option[Ident]
) {
def channelId: Ident =
channelMail
.orElse(channelGotify)
.orElse(channelMatrix)
.orElse(channelHttp)
.getOrElse(
sys.error(s"Illegal internal state: notification hook has no channel: $this")
)
def channelType: ChannelType =
channelMail
.map(_ => ChannelType.Mail)
.orElse(channelGotify.map(_ => ChannelType.Gotify))
.orElse(channelMatrix.map(_ => ChannelType.Matrix))
.orElse(channelHttp.map(_ => ChannelType.Http))
.getOrElse(
sys.error(s"Illegal internal state: notification hook has no channel: $this")
)
}
object RNotificationHookChannel {
def fromRef(id: Ident, hookId: Ident, ref: ChannelRef): RNotificationHookChannel =
ref.channelType match {
case ChannelType.Mail => mail(id, hookId, ref.id)
case ChannelType.Gotify => gotify(id, hookId, ref.id)
case ChannelType.Matrix => matrix(id, hookId, ref.id)
case ChannelType.Http => http(id, hookId, ref.id)
}
def mail(
id: Ident,
hookId: Ident,
channelMail: Ident
): RNotificationHookChannel =
RNotificationHookChannel(
id,
hookId,
channelMail.some,
None,
None,
None
)
def gotify(
id: Ident,
hookId: Ident,
channelGotify: Ident
): RNotificationHookChannel =
RNotificationHookChannel(
id,
hookId,
None,
channelGotify.some,
None,
None
)
def matrix(
id: Ident,
hookId: Ident,
channelMatrix: Ident
): RNotificationHookChannel =
RNotificationHookChannel(
id,
hookId,
None,
None,
channelMatrix.some,
None
)
def http(
id: Ident,
hookId: Ident,
channelHttp: Ident
): RNotificationHookChannel =
RNotificationHookChannel(
id,
hookId,
None,
None,
None,
channelHttp.some
)
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "notification_hook_channel"
val id = Column[Ident]("id", this)
val hookId = Column[Ident]("hook_id", this)
val channelMail = Column[Ident]("channel_mail", this)
val channelGotify = Column[Ident]("channel_gotify", this)
val channelMatrix = Column[Ident]("channel_matrix", this)
val channelHttp = Column[Ident]("channel_http", this)
val all: Nel[Column[_]] =
Nel.of(id, hookId, channelMail, channelGotify, channelMatrix, channelHttp)
}
def as(alias: String): Table =
Table(Some(alias))
val T: Table = Table(None)
def insert(r: RNotificationHookChannel): ConnectionIO[Int] =
DML.insert(
T,
T.all,
sql"${r.id},${r.hookId},${r.channelMail},${r.channelGotify},${r.channelMatrix},${r.channelHttp}"
)
def update(r: RNotificationHookChannel): ConnectionIO[Int] =
DML.update(
T,
T.id === r.id && T.hookId === r.hookId,
DML.set(
T.channelMail.setTo(r.channelMail),
T.channelGotify.setTo(r.channelGotify),
T.channelMatrix.setTo(r.channelMatrix),
T.channelHttp.setTo(r.channelHttp)
)
)
def deleteByHook(hookId: Ident): ConnectionIO[Int] =
DML.delete(T, T.hookId === hookId)
def insertAll(rs: List[RNotificationHookChannel]): ConnectionIO[Int] =
rs.traverse(insert).map(_.sum)
def updateAll(hookId: Ident, channels: List[ChannelRef]): ConnectionIO[Int] =
channels
.traverse(ref => Ident.randomId[ConnectionIO].map(id => fromRef(id, hookId, ref)))
.flatMap(all => deleteByHook(hookId) *> insertAll(all))
def allOf(hookId: Ident): ConnectionIO[Vector[RNotificationHookChannel]] =
Select(select(T.all), from(T), T.hookId === hookId).build
.query[RNotificationHookChannel]
.to[Vector]
def allOfNel(hookId: Ident): ConnectionIO[Nel[RNotificationHookChannel]] =
allOf(hookId)
.map(Nel.fromFoldable[Vector, RNotificationHookChannel])
.flatMap(
_.map(_.pure[ConnectionIO]).getOrElse(
Sync[ConnectionIO]
.raiseError(new Exception(s"Hook '${hookId.id}' has no associated channels!"))
)
)
def resolveRefs(rs: Nel[RNotificationHookChannel]): ConnectionIO[List[ChannelRef]] = {
val cmail = RNotificationChannelMail.as("cmail")
val cgotify = RNotificationChannelGotify.as("cgotify")
val cmatrix = RNotificationChannelMatrix.as("cmatrix")
val chttp = RNotificationChannelHttp.as("chttp")
def selectRef(
idList: List[Ident],
idCol: Column[Ident],
nameCol: Column[String],
ctype: ChannelType,
table: TableDef
) =
Nel
.fromList(idList)
.map(ids =>
Select(
select(idCol.s, const(ctype.name), nameCol.s),
from(table),
idCol.in(ids)
)
)
val mailRefs = selectRef(
rs.toList.flatMap(_.channelMail),
cmail.id,
cmail.name,
ChannelType.Mail,
cmail
)
val gotifyRefs = selectRef(
rs.toList.flatMap(_.channelGotify),
cgotify.id,
cgotify.name,
ChannelType.Gotify,
cgotify
)
val matrixRefs = selectRef(
rs.toList.flatMap(_.channelMatrix),
cmatrix.id,
cmatrix.name,
ChannelType.Matrix,
cmatrix
)
val httpRefs = selectRef(
rs.toList.flatMap(_.channelHttp),
chttp.id,
chttp.name,
ChannelType.Http,
chttp
)
val queries = List(mailRefs, gotifyRefs, matrixRefs, httpRefs).flatten
Nel.fromList(queries) match {
case Some(nel) => union(nel.head, nel.tail: _*).build.query[ChannelRef].to[List]
case None => List.empty[ChannelRef].pure[ConnectionIO]
}
}
}