From 62ef0f7790f226019d56f93e12f64cef1e897f72 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sun, 5 Jan 2020 01:51:05 +0100 Subject: [PATCH] =?utf8?q?=F0=9F=9A=A7=20Send=20event=20when=20mention=20i?= =?utf8?q?s=20removed?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../core/event/MentionOfLocalSoneRemovedEvent.kt | 27 ++++ .../sone/database/PostReplyProvider.kt | 3 + .../pterodactylus/sone/text/SoneMentionDetector.kt | 48 +++++++- .../sone/text/SoneMentionDetectorTest.kt | 137 ++++++++++++++++++--- .../notification/NotificationHandlerModuleTest.kt | 2 + 5 files changed, 193 insertions(+), 24 deletions(-) create mode 100644 src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt new file mode 100644 index 0000000..4898de9 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt @@ -0,0 +1,27 @@ +/** + * Sone - MentionOfLocalSoneRemovedEvent.kt - Copyright © 2019 David ‘Bombe’ Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.core.event + +import net.pterodactylus.sone.data.* + +/** + * Event that signals that a post or reply that mentioned a local Sone was + * removed so that the given post and its replies do not contain a mention of + * a local Sone anymore. + */ +data class MentionOfLocalSoneRemovedEvent(val post: Post) diff --git a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt index cc797d7..4cd0d82 100644 --- a/src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt +++ b/src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt @@ -17,11 +17,14 @@ package net.pterodactylus.sone.database +import com.google.inject.* import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.database.memory.* /** * Interface for objects that can provide [PostReply]s. */ +@ImplementedBy(MemoryDatabase::class) interface PostReplyProvider { fun getPostReply(id: String): PostReply? diff --git a/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt b/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt index f3d8cb4..e816c8e 100644 --- a/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt +++ b/src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt @@ -19,6 +19,8 @@ package net.pterodactylus.sone.text import com.google.common.eventbus.* import net.pterodactylus.sone.core.event.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.database.* import net.pterodactylus.sone.utils.* import javax.inject.* @@ -27,14 +29,14 @@ import javax.inject.* * texts and emits a [MentionOfLocalSoneFoundEvent] if a [SoneTextParser] * finds a [SonePart] that points to a local [Sone]. */ -class SoneMentionDetector @Inject constructor(private val eventBus: EventBus, private val soneTextParser: SoneTextParser) { +class SoneMentionDetector @Inject constructor(private val eventBus: EventBus, private val soneTextParser: SoneTextParser, private val postReplyProvider: PostReplyProvider) { @Subscribe fun onNewPost(newPostFoundEvent: NewPostFoundEvent) { newPostFoundEvent.post.let { post -> post.sone.isLocal.onFalse { - val parts = soneTextParser.parse(post.text, null) - if (parts.filterIsInstance().any { it.sone.isLocal }) { + if (post.text.hasLinksToLocalSones()) { + mentionedPosts += post eventBus.post(MentionOfLocalSoneFoundEvent(post)) } } @@ -45,11 +47,47 @@ class SoneMentionDetector @Inject constructor(private val eventBus: EventBus, pr fun onNewPostReply(event: NewPostReplyFoundEvent) { event.postReply.let { postReply -> postReply.sone.isLocal.onFalse { - if (soneTextParser.parse(postReply.text, null).filterIsInstance().any { it.sone.isLocal }) { - postReply.post.let(::MentionOfLocalSoneFoundEvent).also(eventBus::post) + if (postReply.text.hasLinksToLocalSones()) { + postReply.post + .also { mentionedPosts += it } + .let(::MentionOfLocalSoneFoundEvent).also(eventBus::post) } } } } + @Subscribe + fun onPostRemoved(event: PostRemovedEvent) { + unmentionPost(event.post) + } + + @Subscribe + fun onPostMarkedKnown(event: MarkPostKnownEvent) { + unmentionPost(event.post) + } + + @Subscribe + fun onReplyRemoved(event: PostReplyRemovedEvent) { + event.postReply.post.let { + if ((!it.text.hasLinksToLocalSones() || it.isKnown) && (it.replies.filterNot { it == event.postReply }.none { it.text.hasLinksToLocalSones() && !it.isKnown })) { + unmentionPost(it) + } + } + } + + private fun unmentionPost(post: Post) { + if (post in mentionedPosts) { + eventBus.post(MentionOfLocalSoneRemovedEvent(post)) + mentionedPosts -= post + } + } + + private val mentionedPosts = mutableSetOf() + + private fun String.hasLinksToLocalSones() = soneTextParser.parse(this, null) + .filterIsInstance() + .any { it.sone.isLocal } + + private val Post.replies get() = postReplyProvider.getReplies(id) + } diff --git a/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt b/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt index a56e5b5..24d2335 100644 --- a/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt @@ -31,20 +31,28 @@ import kotlin.test.* /** * Unit test for [SoneMentionDetector]. */ +@Suppress("UnstableApiUsage") class SoneMentionDetectorTest { private val eventBus = EventBus() private val soneProvider = TestSoneProvider() private val postProvider = TestPostProvider() private val soneTextParser = SoneTextParser(soneProvider, postProvider) - private val capturedEvents = mutableListOf() + private val capturedFoundEvents = mutableListOf() + private val capturedRemovedEvents = mutableListOf() + private val postReplyProvider = TestPostReplyProvider() init { - eventBus.register(SoneMentionDetector(eventBus, soneTextParser)) + eventBus.register(SoneMentionDetector(eventBus, soneTextParser, postReplyProvider)) eventBus.register(object : Any() { @Subscribe - fun captureEvent(mentionOfLocalSoneFoundEvent: MentionOfLocalSoneFoundEvent) { - capturedEvents += mentionOfLocalSoneFoundEvent + fun captureFoundEvent(mentionOfLocalSoneFoundEvent: MentionOfLocalSoneFoundEvent) { + capturedFoundEvents += mentionOfLocalSoneFoundEvent + } + + @Subscribe + fun captureRemovedEvent(event: MentionOfLocalSoneRemovedEvent) { + capturedRemovedEvents += event } }) } @@ -53,56 +61,56 @@ class SoneMentionDetectorTest { fun `detector does not emit event on post that does not contain any sones`() { val post = createPost() eventBus.post(NewPostFoundEvent(post)) - assertThat(capturedEvents, emptyIterable()) + assertThat(capturedFoundEvents, emptyIterable()) } @Test fun `detector does not emit event on post that does contain two remote sones`() { val post = createPost("text mentions sone://${remoteSone1.id} and sone://${remoteSone2.id}.") eventBus.post(NewPostFoundEvent(post)) - assertThat(capturedEvents, emptyIterable()) + assertThat(capturedFoundEvents, emptyIterable()) } @Test fun `detector emits event on post that contains links to a remote and a local sone`() { val post = createPost("text mentions sone://${localSone1.id} and sone://${remoteSone2.id}.") eventBus.post(NewPostFoundEvent(post)) - assertThat(capturedEvents, contains(MentionOfLocalSoneFoundEvent(post))) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) } @Test fun `detector emits one event on post that contains two links to the same local sone`() { val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone1.id}.") eventBus.post(NewPostFoundEvent(post)) - assertThat(capturedEvents, contains(MentionOfLocalSoneFoundEvent(post))) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) } @Test fun `detector emits one event on post that contains links to two local sones`() { val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone2.id}.") eventBus.post(NewPostFoundEvent(post)) - assertThat(capturedEvents, contains(MentionOfLocalSoneFoundEvent(post))) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) } @Test fun `detector does not emit event for post by local sone`() { val post = createPost("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", localSone1) eventBus.post(NewPostFoundEvent(post)) - assertThat(capturedEvents, emptyIterable()) + assertThat(capturedFoundEvents, emptyIterable()) } @Test fun `detector does not emit event for reply that contains no sones`() { val reply = emptyPostReply() eventBus.post(NewPostReplyFoundEvent(reply)) - assertThat(capturedEvents, emptyIterable()) + assertThat(capturedFoundEvents, emptyIterable()) } @Test fun `detector does not emit event for reply that contains two links to remote sones`() { val reply = emptyPostReply("text mentions sone://${remoteSone1.id} and sone://${remoteSone2.id}.") eventBus.post(NewPostReplyFoundEvent(reply)) - assertThat(capturedEvents, emptyIterable()) + assertThat(capturedFoundEvents, emptyIterable()) } @Test @@ -110,7 +118,7 @@ class SoneMentionDetectorTest { val post = createPost() val reply = emptyPostReply("text mentions sone://${remoteSone1.id} and sone://${localSone1.id}.", post) eventBus.post(NewPostReplyFoundEvent(reply)) - assertThat(capturedEvents, contains(MentionOfLocalSoneFoundEvent(post))) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) } @Test @@ -118,7 +126,7 @@ class SoneMentionDetectorTest { val post = createPost() val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone1.id}.", post) eventBus.post(NewPostReplyFoundEvent(reply)) - assertThat(capturedEvents, contains(MentionOfLocalSoneFoundEvent(post))) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) } @Test @@ -126,14 +134,94 @@ class SoneMentionDetectorTest { val post = createPost() val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", post) eventBus.post(NewPostReplyFoundEvent(reply)) - assertThat(capturedEvents, contains(MentionOfLocalSoneFoundEvent(post))) + assertThat(capturedFoundEvents, contains(MentionOfLocalSoneFoundEvent(post))) } @Test fun `detector does not emit event for reply by local sone`() { val reply = emptyPostReply("text mentions sone://${localSone1.id} and sone://${localSone2.id}.", sone = localSone1) eventBus.post(NewPostReplyFoundEvent(reply)) - assertThat(capturedEvents, emptyIterable()) + assertThat(capturedFoundEvents, emptyIterable()) + } + + @Test + fun `detector does not emit removed event when a post without mention is removed`() { + val post = createPost() + eventBus.post(PostRemovedEvent(post)) + assertThat(capturedRemovedEvents, emptyIterable()) + } + + @Test + fun `detector does emit removed event when post with mention is removed`() { + val post = createPost("sone://${localSone1.id}") + eventBus.post(NewPostFoundEvent(post)) + eventBus.post(PostRemovedEvent(post)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + + @Test + fun `detector does not emit removed event when a post without mention is marked as known`() { + val post = createPost() + eventBus.post(MarkPostKnownEvent(post)) + assertThat(capturedRemovedEvents, emptyIterable()) + } + + @Test + fun `detector does emit removed event when post with mention is marked as known`() { + val post = createPost("sone://${localSone1.id}") + eventBus.post(NewPostFoundEvent(post)) + eventBus.post(MarkPostKnownEvent(post)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + + @Test + fun `detector does emit removed event when reply with mention is removed and no more mentions in that post exist`() { + val post = createPost() + val reply = emptyPostReply("sone://${localSone1.id}", post) + postReplyProvider.postReplies[post.id] = listOf(reply) + eventBus.post(NewPostReplyFoundEvent(reply)) + eventBus.post(PostReplyRemovedEvent(reply)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + + @Test + fun `detector does not emit removed event when reply with mention is removed and post mentions local sone`() { + val post = createPost("sone://${localSone1.id}") + val reply = emptyPostReply("sone://${localSone1.id}", post) + eventBus.post(NewPostReplyFoundEvent(reply)) + eventBus.post(PostReplyRemovedEvent(reply)) + assertThat(capturedRemovedEvents, emptyIterable()) + } + + @Test + fun `detector does emit removed event when reply with mention is removed and post mentions local sone but is known`() { + val post = createPost("sone://${localSone1.id}", known = true) + val reply = emptyPostReply("sone://${localSone1.id}", post) + eventBus.post(NewPostReplyFoundEvent(reply)) + eventBus.post(PostReplyRemovedEvent(reply)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) + } + + @Test + fun `detector does not emit removed event when reply with mention is removed and post has other replies with mentions`() { + val post = createPost() + val reply1 = emptyPostReply("sone://${localSone1.id}", post) + val reply2 = emptyPostReply("sone://${localSone1.id}", post) + postReplyProvider.postReplies[post.id] = listOf(reply1, reply2) + eventBus.post(NewPostReplyFoundEvent(reply1)) + eventBus.post(PostReplyRemovedEvent(reply1)) + assertThat(capturedRemovedEvents, emptyIterable()) + } + + @Test + fun `detector does emit removed event when reply with mention is removed and post has other replies with mentions which are known`() { + val post = createPost() + val reply1 = emptyPostReply("sone://${localSone1.id}", post) + val reply2 = emptyPostReply("sone://${localSone1.id}", post, known = true) + postReplyProvider.postReplies[post.id] = listOf(reply1, reply2) + eventBus.post(NewPostReplyFoundEvent(reply1)) + eventBus.post(PostReplyRemovedEvent(reply1)) + assertThat(capturedRemovedEvents, contains(MentionOfLocalSoneRemovedEvent(post))) } } @@ -144,10 +232,11 @@ private val remoteSone2 = createRemoteSone() private val localSone1 = createLocalSone() private val localSone2 = createLocalSone() -private fun createPost(text: String = "", sone: Sone = remoteSone1): Post.EmptyPost { +private fun createPost(text: String = "", sone: Sone = remoteSone1, known: Boolean = false): Post.EmptyPost { return object : Post.EmptyPost("post-id") { override fun getSone() = sone override fun getText() = text + override fun isKnown() = known } } @@ -170,13 +259,23 @@ private class TestPostProvider : PostProvider { } -private fun emptyPostReply(text: String = "", post: Post = createPost(), sone: Sone = remoteSone1) = object : PostReply { +private class TestPostReplyProvider : PostReplyProvider { + + val replies = mutableMapOf() + val postReplies = mutableMapOf>() + + override fun getPostReply(id: String) = replies[id] + override fun getReplies(postId: String) = postReplies[postId] ?: emptyList() + +} + +private fun emptyPostReply(text: String = "", post: Post = createPost(), sone: Sone = remoteSone1, known: Boolean = false) = object : PostReply { override val id = "reply-id" override fun getSone() = sone override fun getPostId() = post.id override fun getPost(): Optional = of(post) override fun getTime() = 1L override fun getText() = text - override fun isKnown() = false + override fun isKnown() = known override fun setKnown(known: Boolean): PostReply = this } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt index 7d96208..2606040 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt @@ -25,6 +25,7 @@ import net.pterodactylus.sone.core.event.* import net.pterodactylus.sone.data.* import net.pterodactylus.sone.data.Post.* import net.pterodactylus.sone.data.impl.* +import net.pterodactylus.sone.database.* import net.pterodactylus.sone.freenet.wot.* import net.pterodactylus.sone.main.* import net.pterodactylus.sone.notify.* @@ -59,6 +60,7 @@ class NotificationHandlerModuleTest { WebOfTrustConnector::class.isProvidedBy(webOfTrustConnector), ScheduledExecutorService::class.withNameIsProvidedBy(ticker, "notification"), SoneTextParser::class.isProvidedByMock(), + PostReplyProvider::class.isProvidedByMock(), NotificationHandlerModule() ) -- 2.7.4