diff --git a/frontends/slack/src/main/kotlin/com/gatehill/corebot/chat/SlackChatServiceImpl.kt b/frontends/slack/src/main/kotlin/com/gatehill/corebot/chat/SlackChatServiceImpl.kt index 1bd2be5b..4302d245 100644 --- a/frontends/slack/src/main/kotlin/com/gatehill/corebot/chat/SlackChatServiceImpl.kt +++ b/frontends/slack/src/main/kotlin/com/gatehill/corebot/chat/SlackChatServiceImpl.kt @@ -2,10 +2,13 @@ package com.gatehill.corebot.chat import com.gatehill.corebot.action.model.TriggerContext import com.gatehill.corebot.config.ChatSettings +import com.google.common.cache.CacheBuilder import com.ullink.slack.simpleslackapi.SlackSession +import com.ullink.slack.simpleslackapi.events.SlackMessagePosted import com.ullink.slack.simpleslackapi.listeners.SlackMessagePostedListener import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger +import java.util.concurrent.TimeUnit import java.util.regex.Pattern import javax.inject.Inject @@ -20,6 +23,14 @@ open class SlackChatServiceImpl @Inject constructor(private val sessionService: private val logger: Logger = LogManager.getLogger(SlackChatServiceImpl::class.java) private val messageMatcher = Pattern.compile("<@(?[a-zA-Z0-9]+)>:?(\\s(?.+))?") + /** + * Caches message IDs for de-duplication purposes. + * This is a work-around for a Slack library bug: [https://github.com/Ullink/simple-slack-api/issues/180] + */ + private val messageIdCache = CacheBuilder.newBuilder() + .expireAfterAccess(ChatSettings.chat.messageIdCache, TimeUnit.SECONDS) + .build() + override fun listenForEvents() { messagePostedListeners.forEach { sessionService.session.addMessagePostedListener(it) } } @@ -40,6 +51,9 @@ open class SlackChatServiceImpl @Inject constructor(private val sessionService: // ignore own messages if (session.sessionPersona().id == event.sender.id) return@SlackMessagePostedListener + // filter duplicates + if (isDuplicate(event, session)) return@SlackMessagePostedListener + val trigger = TriggerContext(event.channel.id, event.sender.id, event.sender.userName, event.timestamp, event.threadTimestamp) try { @@ -64,6 +78,26 @@ open class SlackChatServiceImpl @Inject constructor(private val sessionService: } }) + /** + * Determine whether this message has been processed already. + */ + private fun isDuplicate(event: SlackMessagePosted, session: SlackSession): Boolean { + // According to https://api.slack.com/events-api + // 'The combination of event_ts, team_id, user_id, or channel_id is + // intended to be unique. This field is included with every inner event type'. + val uniqueId = "${event.timeStamp}_${session.team.id}_${event.sender?.id}_${event.channel?.id}" + + synchronized(messageIdCache) { + val found = (uniqueId == messageIdCache.getIfPresent(uniqueId)) + if (found) { + logger.debug("Duplicate message received: $event") + } else { + messageIdCache.put(uniqueId, uniqueId) + } + return found + } + } + /*** * Execute the `block` if the message is addressed to the bot and return its result. */ diff --git a/frontends/slack/src/main/kotlin/com/gatehill/corebot/config/ChatSettings.kt b/frontends/slack/src/main/kotlin/com/gatehill/corebot/config/ChatSettings.kt index b5cac235..815a171c 100644 --- a/frontends/slack/src/main/kotlin/com/gatehill/corebot/config/ChatSettings.kt +++ b/frontends/slack/src/main/kotlin/com/gatehill/corebot/config/ChatSettings.kt @@ -12,6 +12,11 @@ object ChatSettings { (System.getenv("SLACK_CHANNELS") ?: "corebot").split(",").map(String::trim) } val postJoinMessage by lazy { System.getenv("SLACK_ENABLE_JOIN_MESSAGE")?.toBoolean() ?: true } + + /** + * The time in seconds to cache message IDs for de-duplication purposes. + */ + val messageIdCache by lazy { System.getenv("SLACK_MESSAGE_ID_CACHE")?.toLong() ?: 120 } } class Threads {