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

@ -0,0 +1,62 @@
create table "notification_channel_mail" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"conn_id" varchar(254) not null,
"recipients" varchar(254) not null,
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") on delete cascade,
foreign key ("conn_id") references "useremail"("id") on delete cascade
);
create table "notification_channel_gotify" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"url" varchar(254) not null,
"app_key" varchar(254) not null,
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") on delete cascade
);
create table "notification_channel_matrix" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"home_server" varchar(254) not null,
"room_id" varchar(254) not null,
"access_token" varchar not null,
"message_type" varchar(254) not null,
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") on delete cascade
);
create table "notification_channel_http" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"url" varchar(254) not null,
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") on delete cascade
);
create table "notification_hook" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"enabled" boolean not null,
"channel_mail" varchar(254),
"channel_gotify" varchar(254),
"channel_matrix" varchar(254),
"channel_http" varchar(254),
"all_events" boolean not null,
"event_filter" varchar(500),
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") 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
);
create table "notification_hook_event" (
"id" varchar(254) not null primary key,
"hook_id" varchar(254) not null,
"event_type" varchar(254) not null,
foreign key ("hook_id") references "notification_hook"("id") on delete cascade
);

View File

@ -0,0 +1,62 @@
create table `notification_channel_mail` (
`id` varchar(254) not null primary key,
`uid` varchar(254) not null,
`conn_id` varchar(254) not null,
`recipients` varchar(254) not null,
`created` timestamp not null,
foreign key (`uid`) references `user_`(`uid`) on delete cascade,
foreign key (`conn_id`) references `useremail`(`id`) on delete cascade
);
create table `notification_channel_gotify` (
`id` varchar(254) not null primary key,
`uid` varchar(254) not null,
`url` varchar(254) not null,
`app_key` varchar(254) not null,
`created` timestamp not null,
foreign key (`uid`) references `user_`(`uid`) on delete cascade
);
create table `notification_channel_matrix` (
`id` varchar(254) not null primary key,
`uid` varchar(254) not null,
`home_server` varchar(254) not null,
`room_id` varchar(254) not null,
`access_token` text not null,
`message_type` varchar(254) not null,
`created` timestamp not null,
foreign key (`uid`) references `user_`(`uid`) on delete cascade
);
create table `notification_channel_http` (
`id` varchar(254) not null primary key,
`uid` varchar(254) not null,
`url` varchar(254) not null,
`created` timestamp not null,
foreign key (`uid`) references `user_`(`uid`) on delete cascade
);
create table `notification_hook` (
`id` varchar(254) not null primary key,
`uid` varchar(254) not null,
`enabled` boolean not null,
`channel_mail` varchar(254),
`channel_gotify` varchar(254),
`channel_matrix` varchar(254),
`channel_http` varchar(254),
`all_events` boolean not null,
`event_filter` varchar(500),
`created` timestamp not null,
foreign key (`uid`) references `user_`(`uid`) 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
);
create table `notification_hook_event` (
`id` varchar(254) not null primary key,
`hook_id` varchar(254) not null,
`event_type` varchar(254) not null,
foreign key (`hook_id`) references `notification_hook`(`id`) on delete cascade
);

View File

@ -0,0 +1,62 @@
create table "notification_channel_mail" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"conn_id" varchar(254) not null,
"recipients" varchar(254) not null,
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") on delete cascade,
foreign key ("conn_id") references "useremail"("id") on delete cascade
);
create table "notification_channel_gotify" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"url" varchar(254) not null,
"app_key" varchar(254) not null,
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") on delete cascade
);
create table "notification_channel_matrix" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"home_server" varchar(254) not null,
"room_id" varchar(254) not null,
"access_token" varchar not null,
"message_type" varchar(254) not null,
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") on delete cascade
);
create table "notification_channel_http" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"url" varchar(254) not null,
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") on delete cascade
);
create table "notification_hook" (
"id" varchar(254) not null primary key,
"uid" varchar(254) not null,
"enabled" boolean not null,
"channel_mail" varchar(254),
"channel_gotify" varchar(254),
"channel_matrix" varchar(254),
"channel_http" varchar(254),
"all_events" boolean not null,
"event_filter" varchar(500),
"created" timestamp not null,
foreign key ("uid") references "user_"("uid") 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
);
create table "notification_hook_event" (
"id" varchar(254) not null primary key,
"hook_id" varchar(254) not null,
"event_type" varchar(254) not null,
foreign key ("hook_id") references "notification_hook"("id") on delete cascade
);

View File

@ -0,0 +1,87 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package db.migration
import cats.data.NonEmptyList
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 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 org.flywaydb.core.api.migration.Context
trait MigrationTasks {
def logger: org.log4s.Logger
implicit val jsonEncoder: Encoder[MailAddress] =
Encoder.encodeString.contramap(_.asUnicodeString)
def migrateDueItemTasks: ConnectionIO[Unit] =
for {
tasks <- RPeriodicTask.findByTask(NotifyDueItemsArgs.taskName)
_ <- Sync[ConnectionIO].delay(
logger.info(s"Starting to migrate ${tasks.size} user tasks")
)
_ <- tasks.traverse(migrateDueItemTask1)
_ <- RPeriodicTask.setEnabledByTask(NotifyDueItemsArgs.taskName, false)
} yield ()
def migrateDueItemTask1(old: RPeriodicTask): ConnectionIO[Int] = {
val converted = old.args
.parseJsonAs[NotifyDueItemsArgs]
.leftMap(_.getMessage())
.flatMap(convertArgs)
converted match {
case Right(args) =>
Sync[ConnectionIO].delay(logger.info(s"Converting user task: $old")) *>
RPeriodicTask.updateTask(
old.id,
PeriodicDueItemsArgs.taskName,
args.asJson.noSpaces
)
case Left(err) =>
logger.error(s"Error converting user task: $old. $err")
0.pure[ConnectionIO]
}
}
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(""), old.smtpConnection, rec)),
old.remindDays,
old.daysBack,
old.tagsInclude,
old.tagsExclude,
old.itemDetailUrl
)
}
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,23 @@
/*
* 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
import org.flywaydb.core.api.migration.Context
class V1_29_2__MigrateNotifyTask extends BaseJavaMigration with MigrationTasks {
val logger = org.log4s.getLogger
override def migrate(ctx: Context): Unit = {
val xa = mkTransactor(ctx)
migrateDueItemTasks.transact(xa).unsafeRunSync()
}
}

View File

@ -0,0 +1,23 @@
/*
* 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
import org.flywaydb.core.api.migration.Context
class V1_29_2__MigrateNotifyTask extends BaseJavaMigration with MigrationTasks {
val logger = org.log4s.getLogger
override def migrate(ctx: Context): Unit = {
val xa = mkTransactor(ctx)
migrateDueItemTasks.transact(xa).unsafeRunSync()
}
}

View File

@ -0,0 +1,23 @@
/*
* 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
import org.flywaydb.core.api.migration.Context
class V1_29_2__MigrateNotifyTask extends BaseJavaMigration with MigrationTasks {
val logger = org.log4s.getLogger
override def migrate(ctx: Context): Unit = {
val xa = mkTransactor(ctx)
migrateDueItemTasks.transact(xa).unsafeRunSync()
}
}

View File

@ -11,6 +11,8 @@ import java.time.{Instant, LocalDate}
import docspell.common._
import docspell.common.syntax.all._
import docspell.jsonminiq.JsonMiniQuery
import docspell.notification.api.EventType
import docspell.query.{ItemQuery, ItemQueryParser}
import docspell.totp.Key
@ -148,6 +150,12 @@ trait DoobieMeta extends EmilDoobieMeta {
Meta[String].timap(s => ItemQueryParser.parseUnsafe(s))(q =>
q.raw.getOrElse(ItemQueryParser.unsafeAsString(q.expr))
)
implicit val metaEventType: Meta[EventType] =
Meta[String].timap(EventType.unsafeFromString)(_.name)
implicit val metaJsonMiniQuery: Meta[JsonMiniQuery] =
Meta[String].timap(JsonMiniQuery.unsafeParse)(_.unsafeAsString)
}
object DoobieMeta extends DoobieMeta {

View File

@ -22,12 +22,12 @@ object FlywayMigrate {
logger.info("Running db migrations...")
val locations = jdbc.dbmsName match {
case Some(dbtype) =>
List(s"classpath:db/migration/$dbtype")
List(s"classpath:db/migration/$dbtype", "classpath:db/migration/common")
case None =>
logger.warn(
s"Cannot read database name from jdbc url: ${jdbc.url}. Go with H2"
)
List("classpath:db/h2")
List("classpath:db/migration/h2", "classpath:db/migration/common")
}
logger.info(s"Using migration locations: $locations")

View File

@ -0,0 +1,44 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.queries
import cats.data.{NonEmptyList, OptionT}
import cats.effect._
import docspell.notification.api.NotificationChannel
import docspell.store.records._
import doobie.ConnectionIO
object ChannelMap {
def readMail(r: RNotificationChannelMail): ConnectionIO[Vector[NotificationChannel]] =
(for {
em <- OptionT(RUserEmail.getById(r.connection))
rec <- OptionT.fromOption[ConnectionIO](NonEmptyList.fromList(r.recipients))
ch = NotificationChannel.Email(em.toMailConfig, em.mailFrom, rec)
} yield Vector(ch)).getOrElse(Vector.empty)
def readGotify(
r: RNotificationChannelGotify
): ConnectionIO[Vector[NotificationChannel]] =
pure(NotificationChannel.Gotify(r.url, r.appKey))
def readMatrix(
r: RNotificationChannelMatrix
): ConnectionIO[Vector[NotificationChannel]] =
pure(NotificationChannel.Matrix(r.homeServer, r.roomId, r.accessToken, r.messageType))
def readHttp(
r: RNotificationChannelHttp
): ConnectionIO[Vector[NotificationChannel]] =
pure(NotificationChannel.HttpPost(r.url, Map.empty))
private def pure[A](a: A): ConnectionIO[Vector[A]] =
Sync[ConnectionIO].pure(Vector(a))
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.queries
import cats.Monad
import cats.data.OptionT
import cats.implicits._
import docspell.common._
import docspell.notification.api._
import docspell.store.qb.DSL._
import docspell.store.qb.Select
import docspell.store.records._
import doobie._
object QNotification {
private val hook = RNotificationHook.as("nh")
private val hevent = RNotificationHookEvent.as("ne")
private val user = RUser.as("u")
def findChannelsForEvent(event: Event): ConnectionIO[Vector[HookChannel]] =
for {
hooks <- listHooks(event.account.collective, event.eventType)
chs <- hooks.traverse(readHookChannel)
} yield chs
// --
final case class HookChannel(
hook: RNotificationHook,
channels: Vector[NotificationChannel]
)
def listHooks(
collective: Ident,
eventType: EventType
): ConnectionIO[Vector[RNotificationHook]] =
run(
select(hook.all),
from(hook).leftJoin(hevent, hevent.hookId === hook.id),
hook.enabled === true && (hook.allEvents === true || hevent.eventType === eventType) && hook.uid
.in(
Select(select(user.uid), from(user), user.cid === collective)
)
).query[RNotificationHook].to[Vector]
def readHookChannel(
hook: RNotificationHook
): ConnectionIO[HookChannel] =
for {
c1 <- read(hook.channelMail)(RNotificationChannelMail.getById)(
ChannelMap.readMail
)
c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById)(
ChannelMap.readGotify
)
c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById)(
ChannelMap.readMatrix
)
c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById)(ChannelMap.readHttp)
} yield HookChannel(hook, c1 ++ c2 ++ c3 ++ c4)
def readChannel(ch: RNotificationChannel): ConnectionIO[Vector[NotificationChannel]] =
ch.fold(
ChannelMap.readMail,
ChannelMap.readGotify,
ChannelMap.readMatrix,
ChannelMap.readHttp
)
private def read[A, B](channel: Option[Ident])(
load: Ident => ConnectionIO[Option[A]]
)(
m: A => ConnectionIO[Vector[B]]
): ConnectionIO[Vector[B]] =
channel match {
case Some(ch) =>
(for {
a <- OptionT(load(ch))
ch <- OptionT.liftF(m(a))
} yield ch).getOrElse(Vector.empty)
case None =>
Monad[ConnectionIO].pure(Vector.empty)
}
}

View File

@ -410,6 +410,14 @@ object RItem {
def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] =
run(select(T.all), from(T), T.id === itemId && T.cid === coll).query[RItem].option
def findAllByIdAndCollective(
itemIds: NonEmptyList[Ident],
coll: Ident
): ConnectionIO[Vector[RItem]] =
run(select(T.all), from(T), T.id.in(itemIds) && T.cid === coll)
.query[RItem]
.to[Vector]
def findById(itemId: Ident): ConnectionIO[Option[RItem]] =
run(select(T.all), from(T), T.id === itemId).query[RItem].option

View File

@ -0,0 +1,148 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.OptionT
import docspell.common._
import docspell.notification.api.ChannelRef
import docspell.notification.api.ChannelType
import doobie._
sealed trait RNotificationChannel {
def id: Ident
def fold[A](
f1: RNotificationChannelMail => A,
f2: RNotificationChannelGotify => A,
f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A
): A
}
object RNotificationChannel {
final case class Email(r: RNotificationChannelMail) extends RNotificationChannel {
override def fold[A](
f1: RNotificationChannelMail => A,
f2: RNotificationChannelGotify => A,
f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A
): A = f1(r)
val id = r.id
}
final case class Gotify(r: RNotificationChannelGotify) extends RNotificationChannel {
override def fold[A](
f1: RNotificationChannelMail => A,
f2: RNotificationChannelGotify => A,
f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A
): A = f2(r)
val id = r.id
}
final case class Matrix(r: RNotificationChannelMatrix) extends RNotificationChannel {
override def fold[A](
f1: RNotificationChannelMail => A,
f2: RNotificationChannelGotify => A,
f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A
): A = f3(r)
val id = r.id
}
final case class Http(r: RNotificationChannelHttp) extends RNotificationChannel {
override def fold[A](
f1: RNotificationChannelMail => A,
f2: RNotificationChannelGotify => A,
f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A
): A = f4(r)
val id = r.id
}
def insert(r: RNotificationChannel): ConnectionIO[Int] =
r.fold(
RNotificationChannelMail.insert,
RNotificationChannelGotify.insert,
RNotificationChannelMatrix.insert,
RNotificationChannelHttp.insert
)
def update(r: RNotificationChannel): ConnectionIO[Int] =
r.fold(
RNotificationChannelMail.update,
RNotificationChannelGotify.update,
RNotificationChannelMatrix.update,
RNotificationChannelHttp.update
)
def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannel]] =
for {
mail <- RNotificationChannelMail.getByAccount(account)
gotify <- RNotificationChannelGotify.getByAccount(account)
matrix <- RNotificationChannelMatrix.getByAccount(account)
http <- RNotificationChannelHttp.getByAccount(account)
} yield mail.map(Email.apply) ++ gotify.map(Gotify.apply) ++ matrix.map(
Matrix.apply
) ++ http.map(Http.apply)
def getById(id: Ident): ConnectionIO[Vector[RNotificationChannel]] =
for {
mail <- RNotificationChannelMail.getById(id)
gotify <- RNotificationChannelGotify.getById(id)
matrix <- RNotificationChannelMatrix.getById(id)
http <- RNotificationChannelHttp.getById(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]] =
ref.channelType match {
case ChannelType.Mail =>
RNotificationChannelMail.getById(ref.id).map(_.map(Email.apply))
case ChannelType.Matrix =>
RNotificationChannelMatrix.getById(ref.id).map(_.map(Matrix.apply))
case ChannelType.Gotify =>
RNotificationChannelGotify.getById(ref.id).map(_.map(Gotify.apply))
case ChannelType.Http =>
RNotificationChannelHttp.getById(ref.id).map(_.map(Http.apply))
}
def getByHook(r: 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 deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] =
for {
n1 <- RNotificationChannelMail.deleteByAccount(id, account)
n2 <- RNotificationChannelGotify.deleteByAccount(id, account)
n3 <- RNotificationChannelMatrix.deleteByAccount(id, account)
n4 <- RNotificationChannelHttp.deleteByAccount(id, account)
} yield n1 + n2 + n3 + n4
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.NonEmptyList
import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import doobie._
import doobie.implicits._
final case class RNotificationChannelGotify(
id: Ident,
uid: Ident,
url: LenientUri,
appKey: Password,
created: Timestamp
) {
def vary: RNotificationChannel =
RNotificationChannel.Gotify(this)
}
object RNotificationChannelGotify {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "notification_channel_gotify"
val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this)
val url = Column[LenientUri]("url", this)
val appKey = Column[Password]("app_key", this)
val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, uid, url, appKey, created)
}
val T: Table = Table(None)
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 insert(r: RNotificationChannelGotify): ConnectionIO[Int] =
DML.insert(T, T.all, sql"${r.id},${r.uid},${r.url},${r.appKey},${r.created}")
def update(r: RNotificationChannelGotify): ConnectionIO[Int] =
DML.update(
T,
T.id === r.id && T.uid === r.uid,
DML.set(
T.url.setTo(r.url),
T.appKey.setTo(r.appKey)
)
)
def getByAccount(
account: AccountId
): ConnectionIO[Vector[RNotificationChannelGotify]] = {
val user = RUser.as("u")
val gotify = as("c")
Select(
select(gotify.all),
from(gotify).innerJoin(user, user.uid === gotify.uid),
user.cid === account.collective && user.login === account.user
).build.query[RNotificationChannelGotify].to[Vector]
}
def deleteById(id: Ident): ConnectionIO[Int] =
DML.delete(T, T.id === id)
def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = {
val u = RUser.as("u")
DML.delete(
T,
T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account)))
)
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.NonEmptyList
import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import doobie._
import doobie.implicits._
final case class RNotificationChannelHttp(
id: Ident,
uid: Ident,
url: LenientUri,
created: Timestamp
) {
def vary: RNotificationChannel =
RNotificationChannel.Http(this)
}
object RNotificationChannelHttp {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "notification_channel_http"
val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this)
val url = Column[LenientUri]("url", this)
val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, uid, url, created)
}
val T: Table = Table(None)
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 insert(r: RNotificationChannelHttp): ConnectionIO[Int] =
DML.insert(T, T.all, sql"${r.id},${r.uid},${r.url},${r.created}")
def update(r: RNotificationChannelHttp): ConnectionIO[Int] =
DML.update(T, T.id === r.id && T.uid === r.uid, DML.set(T.url.setTo(r.url)))
def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelHttp]] = {
val user = RUser.as("u")
val http = as("c")
Select(
select(http.all),
from(http).innerJoin(user, user.uid === http.uid),
user.cid === account.collective && user.login === account.user
).build.query[RNotificationChannelHttp].to[Vector]
}
def deleteById(id: Ident): ConnectionIO[Int] =
DML.delete(T, T.id === id)
def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = {
val u = RUser.as("u")
DML.delete(
T,
T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account)))
)
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.NonEmptyList
import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import doobie._
import doobie.implicits._
import emil.MailAddress
final case class RNotificationChannelMail(
id: Ident,
uid: Ident,
connection: Ident,
recipients: List[MailAddress],
created: Timestamp
) {
def vary: RNotificationChannel =
RNotificationChannel.Email(this)
}
object RNotificationChannelMail {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "notification_channel_mail"
val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this)
val connection = Column[Ident]("conn_id", this)
val recipients = Column[List[MailAddress]]("recipients", this)
val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, uid, connection, recipients, created)
}
val T: Table = Table(None)
def as(alias: String): Table = Table(Some(alias))
def insert(r: RNotificationChannelMail): ConnectionIO[Int] =
DML.insert(
T,
T.all,
sql"${r.id},${r.uid},${r.connection},${r.recipients},${r.created}"
)
def update(r: RNotificationChannelMail): ConnectionIO[Int] =
DML.update(
T,
T.id === r.id && T.uid === r.uid,
DML.set(
T.connection.setTo(r.connection),
T.recipients.setTo(r.recipients.toList)
)
)
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] =
run(select(T.all), from(T), T.id === id).query[RNotificationChannelMail].option
def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelMail]] = {
val user = RUser.as("u")
val gotify = as("c")
Select(
select(gotify.all),
from(gotify).innerJoin(user, user.uid === gotify.uid),
user.cid === account.collective && user.login === account.user
).build.query[RNotificationChannelMail].to[Vector]
}
def deleteById(id: Ident): ConnectionIO[Int] =
DML.delete(T, T.id === id)
def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = {
val u = RUser.as("u")
DML.delete(
T,
T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account)))
)
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.NonEmptyList
import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import doobie._
import doobie.implicits._
final case class RNotificationChannelMatrix(
id: Ident,
uid: Ident,
homeServer: LenientUri,
roomId: String,
accessToken: Password,
messageType: String,
created: Timestamp
) {
def vary: RNotificationChannel =
RNotificationChannel.Matrix(this)
}
object RNotificationChannelMatrix {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "notification_channel_matrix"
val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this)
val homeServer = Column[LenientUri]("home_server", this)
val roomId = Column[String]("room_id", this)
val accessToken = Column[Password]("access_token", this)
val messageType = Column[String]("message_type", this)
val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, uid, homeServer, roomId, accessToken, messageType, created)
}
val T: Table = Table(None)
def as(alias: String): Table = Table(Some(alias))
def insert(r: RNotificationChannelMatrix): ConnectionIO[Int] =
DML.insert(
T,
T.all,
sql"${r.id},${r.uid},${r.homeServer},${r.roomId},${r.accessToken},${r.messageType},${r.created}"
)
def update(r: RNotificationChannelMatrix): ConnectionIO[Int] =
DML.update(
T,
T.id === r.id && T.uid === r.uid,
DML.set(
T.homeServer.setTo(r.homeServer),
T.roomId.setTo(r.roomId),
T.accessToken.setTo(r.accessToken),
T.messageType.setTo(r.messageType)
)
)
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMatrix]] =
run(select(T.all), from(T), T.id === id).query[RNotificationChannelMatrix].option
def getByAccount(
account: AccountId
): ConnectionIO[Vector[RNotificationChannelMatrix]] = {
val user = RUser.as("u")
val gotify = as("c")
Select(
select(gotify.all),
from(gotify).innerJoin(user, user.uid === gotify.uid),
user.cid === account.collective && user.login === account.user
).build.query[RNotificationChannelMatrix].to[Vector]
}
def deleteById(id: Ident): ConnectionIO[Int] =
DML.delete(T, T.id === id)
def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = {
val u = RUser.as("u")
DML.delete(
T,
T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account)))
)
}
}

View File

@ -0,0 +1,233 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.NonEmptyList
import cats.implicits._
import docspell.common._
import docspell.jsonminiq.JsonMiniQuery
import docspell.notification.api.EventType
import docspell.store.qb.DSL._
import docspell.store.qb._
import doobie._
import doobie.implicits._
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)
val all: NonEmptyList[Column[_]] =
NonEmptyList.of(
id,
uid,
enabled,
channelMail,
channelGotify,
channelMatrix,
channelHttp,
allEvents,
eventFilter,
created
)
}
val T: Table = Table(None)
def as(alias: String): Table = Table(Some(alias))
def insert(r: RNotificationHook): ConnectionIO[Int] =
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}"
)
def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = {
val u = RUser.as("u")
DML.delete(
T,
T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account)))
)
}
def update(r: RNotificationHook): ConnectionIO[Int] =
DML.update(
T,
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)
)
)
def findByAccount(account: AccountId): ConnectionIO[Vector[RNotificationHook]] =
Select(
select(T.all),
from(T),
T.uid.in(Select(select(RUser.T.uid), from(RUser.T), RUser.T.isAccount(account)))
).build.query[RNotificationHook].to[Vector]
def getById(id: Ident, userId: Ident): ConnectionIO[Option[RNotificationHook]] =
Select(
select(T.all),
from(T),
T.id === id && T.uid === userId
).build.query[RNotificationHook].option
def findAllByAccount(
account: AccountId
): ConnectionIO[Vector[(RNotificationHook, List[EventType])]] = {
val h = RNotificationHook.as("h")
val e = RNotificationHookEvent.as("e")
val userSelect =
Select(select(RUser.T.uid), from(RUser.T), RUser.T.isAccount(account))
val withEvents = Select(
select(h.all :+ e.eventType),
from(h).innerJoin(e, e.hookId === h.id),
h.uid.in(userSelect)
).orderBy(h.id)
.build
.query[(RNotificationHook, EventType)]
.to[Vector]
.map(_.groupBy(_._1).view.mapValues(_.map(_._2).toList).toVector)
val withoutEvents =
Select(
select(h.all),
from(h),
h.id.notIn(Select(select(e.hookId), from(e))) && h.uid.in(userSelect)
).build
.query[RNotificationHook]
.to[Vector]
.map(list => list.map(h => (h, Nil: List[EventType])))
for {
sel1 <- withEvents
sel2 <- withoutEvents
} yield sel1 ++ sel2
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.NonEmptyList
import cats.implicits._
import docspell.common._
import docspell.notification.api.EventType
import docspell.store.qb.DSL._
import docspell.store.qb._
import doobie._
import doobie.implicits._
final case class RNotificationHookEvent(
id: Ident,
hookId: Ident,
eventType: EventType
)
object RNotificationHookEvent {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "notification_hook_event"
val id = Column[Ident]("id", this)
val hookId = Column[Ident]("hook_id", this)
val eventType = Column[EventType]("event_type", this)
val all: NonEmptyList[Column[_]] =
NonEmptyList.of(
id,
hookId,
eventType
)
}
val T: Table = Table(None)
def as(alias: String): Table = Table(Some(alias))
def insert(r: RNotificationHookEvent): ConnectionIO[Int] =
DML.insert(
T,
T.all,
sql"${r.id},${r.hookId},${r.eventType}"
)
def insertAll(hookId: Ident, events: List[EventType]): ConnectionIO[Int] =
events
.traverse(et =>
Ident
.randomId[ConnectionIO]
.flatMap(id => insert(RNotificationHookEvent(id, hookId, et)))
)
.map(_.sum)
def updateAll(hookId: Ident, events: List[EventType]): ConnectionIO[Int] =
deleteByHook(hookId) *> insertAll(hookId, events)
def deleteByHook(hookId: Ident): ConnectionIO[Int] =
DML.delete(T, T.hookId === hookId)
def update(r: RNotificationHookEvent): ConnectionIO[Int] =
DML.update(T, T.id === r.id, DML.set(T.eventType.setTo(r.eventType)))
}

View File

@ -164,6 +164,19 @@ object RPeriodicTask {
def as(alias: String): Table =
Table(Some(alias))
def findByTask(taskName: Ident): ConnectionIO[Vector[RPeriodicTask]] =
Select(
select(T.all),
from(T),
T.task === taskName
).build.query[RPeriodicTask].to[Vector]
def updateTask(id: Ident, taskName: Ident, args: String): ConnectionIO[Int] =
DML.update(T, T.id === id, DML.set(T.task.setTo(taskName), T.args.setTo(args)))
def setEnabledByTask(taskName: Ident, enabled: Boolean): ConnectionIO[Int] =
DML.update(T, T.task === taskName, DML.set(T.enabled.setTo(enabled)))
def insert(v: RPeriodicTask): ConnectionIO[Int] =
DML.insert(
T,

View File

@ -95,11 +95,11 @@ object RTagItem {
)
} yield n
def appendTags(item: Ident, tags: List[Ident]): ConnectionIO[Int] =
def appendTags(item: Ident, tags: List[Ident]): ConnectionIO[Set[Ident]] =
for {
existing <- findByItem(item)
toadd = tags.toSet.diff(existing.map(_.tagId).toSet)
n <- setAllTags(item, toadd.toSeq)
} yield n
_ <- setAllTags(item, toadd.toSeq)
} yield toadd
}

View File

@ -71,6 +71,9 @@ object RUser {
val lastLogin = Column[Timestamp]("lastlogin", this)
val created = Column[Timestamp]("created", this)
def isAccount(aid: AccountId) =
cid === aid.collective && login === aid.user
val all =
NonEmptyList.of[Column[_]](
uid,

View File

@ -176,6 +176,13 @@ object RUserEmail {
run(select(t.all), from(t), t.uid === userId).query[RUserEmail].to[Vector]
}
def getByUser(userId: Ident, name: Ident): ConnectionIO[Option[RUserEmail]] = {
val t = Table(None)
run(select(t.all), from(t), t.uid === userId && t.name === name)
.query[RUserEmail]
.option
}
private def findByAccount0(
accId: AccountId,
nameQ: Option[String],
@ -206,6 +213,11 @@ object RUserEmail {
def getByName(accId: AccountId, name: Ident): ConnectionIO[Option[RUserEmail]] =
findByAccount0(accId, Some(name.id), true).option
def getById(id: Ident): ConnectionIO[Option[RUserEmail]] = {
val t = Table(None)
run(select(t.all), from(t), t.id === id).query[RUserEmail].option
}
def delete(accId: AccountId, connName: Ident): ConnectionIO[Int] = {
val user = RUser.as("u")