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.*
* 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))
}
}
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)
+
}
/**
* 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
}
})
}
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
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
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
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)))
}
}
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
}
}
}
-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
}