Can delete Notification

Add ID to notification
This commit is contained in:
2020-02-28 23:43:51 +01:00
parent f3e0f64249
commit 0421f3cb55
4 changed files with 101 additions and 71 deletions

View File

@@ -13,7 +13,9 @@ import fr.dcproject.Env.PROD
import fr.dcproject.entity.* import fr.dcproject.entity.*
import fr.dcproject.event.EntityEvent import fr.dcproject.event.EntityEvent
import fr.dcproject.event.EventNotification import fr.dcproject.event.EventNotification
import fr.dcproject.event.Notification
import fr.dcproject.event.publisher.Publisher import fr.dcproject.event.publisher.Publisher
import fr.dcproject.repository.Follow
import fr.dcproject.repository.FollowArticle import fr.dcproject.repository.FollowArticle
import fr.dcproject.routes.* import fr.dcproject.routes.*
import fr.dcproject.security.voter.* import fr.dcproject.security.voter.*
@@ -41,12 +43,10 @@ import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import io.lettuce.core.api.async.RedisAsyncCommands import io.lettuce.core.api.async.RedisAsyncCommands
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.eclipse.jetty.util.log.Slf4jLog import org.eclipse.jetty.util.log.Slf4jLog
import org.joda.time.DateTime
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.ktor.ext.Koin import org.koin.ktor.ext.Koin
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
@@ -193,10 +193,10 @@ fun Application.module(env: Env = PROD) {
val exchangeName = config.exchangeNotificationName val exchangeName = config.exchangeNotificationName
get<ConnectionFactory>().newConnection().use { connection -> get<ConnectionFactory>().newConnection().use { connection ->
connection.createChannel().use { channel -> connection.createChannel().use { channel ->
channel.queueDeclare("sse", true, false, false, null) channel.queueDeclare("push", true, false, false, null)
channel.queueDeclare("email", true, false, false, null) channel.queueDeclare("email", true, false, false, null)
channel.exchangeDeclare(exchangeName, DIRECT, true) channel.exchangeDeclare(exchangeName, DIRECT, true)
channel.queueBind("sse", exchangeName, "") channel.queueBind("push", exchangeName, "")
channel.queueBind("email", exchangeName, "") channel.queueBind("email", exchangeName, "")
} }
} }
@@ -208,43 +208,40 @@ fun Application.module(env: Env = PROD) {
} }
/* Launch Consumer */ /* Launch Consumer */
GlobalScope.launch { launch {
val connection = get<ConnectionFactory>().newConnection() val rabbitChannel = get<ConnectionFactory>().newConnection().createChannel()
val channel = connection.createChannel()
val redis = get<RedisAsyncCommands<String, String>>() val redis = get<RedisAsyncCommands<String, String>>()
val consumerSSE: Consumer = object : DefaultConsumer(channel) {
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class) @Throws(IOException::class)
override fun handleDelivery( override fun handleDelivery(
consumerTag: String, consumerTag: String,
envelope: Envelope, envelope: Envelope,
properties: AMQP.BasicProperties, properties: AMQP.BasicProperties,
body: ByteArray body: ByteArray
) { ) = runBlocking {
val message = body.toString(Charsets.UTF_8) val message = body.toString(Charsets.UTF_8)
val event = val msg = message.deserialize<EntityEvent>() ?: error("Unable to unserialise event message from rabbit")
message.deserialize<EntityEvent>() ?: error("Unable to unserialise event message from rabbit")
val followRepo = when (event.type) { let {
"article" -> get<FollowArticle>() when (msg.type) {
else -> error("type of event not supported") Notification.Type.ARTICLE -> get<FollowArticle>()
} as Follow<*,*>
} }
.findFollowsByTarget(msg.target)
runBlocking {
followRepo
.findFollowsByTarget(event.target)
.collect { follow -> .collect { follow ->
redis.zadd( redis.zadd(
"notification:${follow.createdBy.id}", "notification:${follow.createdBy.id}",
DateTime.now().millis.toDouble(), msg.id,
message message
) )
} }
}
channel.basicAck(envelope.deliveryTag, false) rabbitChannel.basicAck(envelope.deliveryTag, false)
} }
} }
val consumerEmail: Consumer = object : DefaultConsumer(channel) { val consumerEmail: Consumer = object : DefaultConsumer(rabbitChannel) {
@Throws(IOException::class) @Throws(IOException::class)
override fun handleDelivery( override fun handleDelivery(
consumerTag: String, consumerTag: String,
@@ -255,11 +252,11 @@ fun Application.module(env: Env = PROD) {
val message = body.toString(Charsets.UTF_8) val message = body.toString(Charsets.UTF_8)
println("The message is receive for send email: $message") println("The message is receive for send email: $message")
// TODO implement email sender // TODO implement email sender
channel.basicAck(envelope.deliveryTag, false) rabbitChannel.basicAck(envelope.deliveryTag, false)
} }
} }
channel.basicConsume("sse", false, consumerSSE) rabbitChannel.basicConsume("push", false, consumerPush) // The front consume the redis via Websocket
channel.basicConsume("email", false, consumerEmail) rabbitChannel.basicConsume("email", false, consumerEmail)
} }
} }

View File

@@ -1,5 +1,6 @@
package fr.dcproject.event package fr.dcproject.event
import com.fasterxml.jackson.annotation.JsonValue
import fr.dcproject.entity.Article import fr.dcproject.entity.Article
import fr.postgresjson.entity.Serializable import fr.postgresjson.entity.Serializable
import fr.postgresjson.entity.immutable.UuidEntity import fr.postgresjson.entity.immutable.UuidEntity
@@ -8,15 +9,27 @@ import io.ktor.util.AttributeKey
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.DisposableHandle
import org.joda.time.DateTime import org.joda.time.DateTime
import kotlin.random.Random.Default.nextInt
sealed class NotificationS
open class Notification( open class Notification(
val type: String, val type: Type,
val createdAt: DateTime = DateTime.now() val createdAt: DateTime = DateTime.now()
) : Serializable ) : NotificationS(), Serializable {
val id: Double = randId(createdAt.millis)
enum class Type(@JsonValue val type: String) {
ARTICLE("article");
}
private fun randId(time: Long): Double {
return (time.toString() + nextInt(1000, 9999).toString()).toDouble()
}
}
open class EntityEvent( open class EntityEvent(
val target: UuidEntity, val target: UuidEntity,
type: String, type: Notification.Type,
val action: String val action: String
) : Notification(type) { ) : Notification(type) {
enum class Type(val event: EventDefinition<ArticleUpdate>) { enum class Type(val event: EventDefinition<ArticleUpdate>) {
@@ -26,7 +39,7 @@ open class EntityEvent(
class ArticleUpdate( class ArticleUpdate(
target: Article target: Article
) : EntityEvent(target, "article", "update") ) : EntityEvent(target, Notification.Type.ARTICLE, "update")
/** /**
* Installation Class * Installation Class

View File

@@ -12,7 +12,7 @@ import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Constitution as ConstitutionEntity import fr.dcproject.entity.Constitution as ConstitutionEntity
import fr.dcproject.entity.Follow as FollowEntity import fr.dcproject.entity.Follow as FollowEntity
open class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Requester) : RepositoryI { sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Requester) : RepositoryI {
open fun findByCitizen( open fun findByCitizen(
citizen: CitizenI, citizen: CitizenI,
page: Int = 1, page: Int = 1,
@@ -63,6 +63,26 @@ open class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Reque
"citizen_id" to citizen.id, "citizen_id" to citizen.id,
"target_id" to target.id "target_id" to target.id
) )
fun findFollowsByTarget(
target: UuidEntity,
bulkSize: Int = 300
): Flow<FollowSimple<IN, CitizenRef>> = flow {
var nextPage = 1
do {
val paginate = findFollowsByTarget(target, nextPage, bulkSize)
paginate.result.forEach {
emit(it)
}
nextPage = paginate.currentPage+1
} while (!paginate.isLastPage())
}
abstract fun findFollowsByTarget(
target: UuidEntity,
page: Int = 1,
limit: Int = 300
): Paginated<FollowSimple<IN, CitizenRef>>
} }
class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleEntity>(requester) { class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleEntity>(requester) {
@@ -80,10 +100,10 @@ class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleEntity>(re
} }
} }
fun findFollowsByTarget( override fun findFollowsByTarget(
target: UuidEntity, target: UuidEntity,
page: Int = 1, page: Int,
limit: Int = 300 limit: Int
): Paginated<FollowSimple<ArticleRef, CitizenRef>> { ): Paginated<FollowSimple<ArticleRef, CitizenRef>> {
return requester return requester
.getFunction("find_follows_article_by_target") .getFunction("find_follows_article_by_target")
@@ -91,20 +111,6 @@ class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleEntity>(re
"target_id" to target.id "target_id" to target.id
) )
} }
fun findFollowsByTarget(
target: UuidEntity,
limit: Int = 300
): Flow<FollowSimple<ArticleRef, CitizenRef>> = flow {
var nextPage = 1
do {
val paginate = findFollowsByTarget(target, nextPage, limit)
paginate.result.forEach {
emit(it)
}
nextPage = paginate.currentPage+1
} while (!paginate.isLastPage())
}
} }
class FollowConstitution(requester: Requester) : Follow<ConstitutionRef, ConstitutionEntity>(requester) { class FollowConstitution(requester: Requester) : Follow<ConstitutionRef, ConstitutionEntity>(requester) {
@@ -121,4 +127,12 @@ class FollowConstitution(requester: Requester) : Follow<ConstitutionRef, Constit
) )
} }
} }
override fun findFollowsByTarget(
target: UuidEntity,
page: Int,
limit: Int
): Paginated<FollowSimple<ConstitutionRef, CitizenRef>> {
TODO("Not yet implemented")
}
} }

View File

@@ -1,6 +1,8 @@
package fr.dcproject.routes package fr.dcproject.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.event.Notification
import fr.postgresjson.serializer.deserialize
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.http.cio.websocket.Frame import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText import io.ktor.http.cio.websocket.readText
@@ -21,7 +23,21 @@ import kotlinx.coroutines.launch
fun Route.notificationArticle(redis: RedisAsyncCommands<String, String>, client: HttpClient) { fun Route.notificationArticle(redis: RedisAsyncCommands<String, String>, client: HttpClient) {
webSocket("/notifications") { webSocket("/notifications") {
val citizenId = call.citizen.id val citizenId = call.citizen.id
val job = launch {
launch {
incoming.consumeAsFlow().mapNotNull { it as? Frame.Text }.collect {
val notificationMessage = it.readText().deserialize<Notification>() ?: error("unable to deserialize message")
redis.zremrangebyscore(
"notification:$citizenId",
Range.from(
Range.Boundary.including(notificationMessage.id),
Range.Boundary.including(notificationMessage.id)
)
)
}
}
var score = 0.0 var score = 0.0
while (!outgoing.isClosedForSend) { while (!outgoing.isClosedForSend) {
val result = redis.zrangebyscoreWithScores( val result = redis.zrangebyscoreWithScores(
@@ -37,16 +53,6 @@ fun Route.notificationArticle(redis: RedisAsyncCommands<String, String>, client:
if (it.score > score) score = it.score if (it.score > score) score = it.score
} }
delay(1000) delay(1000)
// TODO terminate coroutine after connection close !
}
}
job.join()
// TODO mark notification as read
incoming.consumeAsFlow().mapNotNull { it as? Frame.Text }.collect {
val text = it.readText()
outgoing.send(Frame.Text(text))
delay(100)
} }
} }
} }