🚧 Send event when mention is removed
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 5 Jan 2020 00:51:05 +0000 (01:51 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 5 Jan 2020 02:44:49 +0000 (03:44 +0100)
src/main/kotlin/net/pterodactylus/sone/core/event/MentionOfLocalSoneRemovedEvent.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/database/PostReplyProvider.kt
src/main/kotlin/net/pterodactylus/sone/text/SoneMentionDetector.kt
src/test/kotlin/net/pterodactylus/sone/text/SoneMentionDetectorTest.kt
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.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 (file)
index 0000000..4898de9
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ */
+
+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)
index cc797d7..4cd0d82 100644 (file)
 
 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?
index f3d8cb4..e816c8e 100644 (file)
@@ -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<SonePart>().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<SonePart>().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<Post>()
+
+       private fun String.hasLinksToLocalSones() = soneTextParser.parse(this, null)
+                       .filterIsInstance<SonePart>()
+                       .any { it.sone.isLocal }
+
+       private val Post.replies get() = postReplyProvider.getReplies(id)
+
 }
index a56e5b5..24d2335 100644 (file)
@@ -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<MentionOfLocalSoneFoundEvent>()
+       private val capturedFoundEvents = mutableListOf<MentionOfLocalSoneFoundEvent>()
+       private val capturedRemovedEvents = mutableListOf<MentionOfLocalSoneRemovedEvent>()
+       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<String, PostReply>()
+       val postReplies = mutableMapOf<String, List<PostReply>>()
+
+       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<Post> = 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
 }
index 7d96208..2606040 100644 (file)
@@ -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()
        )