Can delete Notification
Add ID to notification
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user