slackapi / bolt-python

A framework to build Slack apps using Python
https://tools.slack.dev/bolt-python/
MIT License
1.06k stars 245 forks source link

I would like to refer to X-Slack-Retry-Num. #731

Closed iwakiri0104 closed 2 years ago

iwakiri0104 commented 2 years ago

I want to deploy a Slack bolt using Cloud Function, but since I can't use lazy listener, I use Thread for parallel processing. When I deploy to Cloud Function with the following code, I get duplicate messages the first time. After that, it only needs to be done once. However, after some time passes, duplicate messages are sent again. Therefore, I would like to implement a process that only returns a response if there is an X-Slack-Retry-Num in the header, but I do not know how to get the header. I implemented the following code in Python this way,

    if "X-Slack-Retry-Num" in event["headers"]:
        return {'statusCode': 200, 'body': 'this request is retry'}

Ended with an error result.

    if "X-Slack-Retry-Num" in event["headers"]:
KeyError: 'headers'

Please let me know if there is a better solution.

 This is my code:

import os
import logging
from slack_bolt import App
from slack_bolt import Say
from slack_bolt.adapter.socket_mode import SocketModeHandler
from slack_sdk import WebClient
import functions_framework
import deepl
import langid
import json
from slack_bolt.adapter.flask import SlackRequestHandler

logging.basicConfig(level=logging.DEBUG)

DEEPL_API_TOKEN = os.environ["DEEPL_API_TOKEN"]
SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]

app = App(
    token=SLACK_BOT_TOKEN,
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
    process_before_response=True
    )
handler = SlackRequestHandler(app)

client = WebClient(SLACK_BOT_TOKEN)
translator = deepl.Translator(DEEPL_API_TOKEN)

@app.middleware
def log_request(logger, body, next):
    logger.debug(body)
    return next()

@app.event("reaction_added")
def show_datepicker(event,say: Say):
    reaction = event["reaction"]
    replies = say.client.conversations_replies(channel=event["item"]["channel"], ts=event["item"]["ts"])
    print("replies is:",replies)
    message = replies["messages"][0]["text"]
    print("message is:",message)
    lang = langid.classify(replies["messages"][0]["text"])
    print("languages[0] is:",lang[0])

    if reaction == "eyes":
        if lang[0] == "ja":
            target_lang = "EN-US"
        elif lang[0] == "en":
            target_lang = "JA"
        else:
            return
        result = translator.translate_text(message, target_lang=target_lang)
        query = ""
        for m in message.split("\n"):
            query += f">{m}\n"
        say(
            thread_ts=replies["messages"][0].get("ts"),
            text=f"{query}{result.text}"
            )           
        return 

from threading import Thread

@functions_framework.http
def slack_bot(request):
    request_data = request.form
    t = Thread(target=show_datepicker, kwargs={'request_data': request_data})
    t.start()
    return handler.handle(request)

if __name__ == "__main__":
    app.start(port=int(os.environ.get("PORT", 3000)))

The slack_bolt version

slack-bolt==1.14.3 slack-sdk==3.18.3

Python runtime version

Python 3.9.12

Expected result:

I want to prevent duplicate messages.

Actual result:

Duplicate messages more than three times

seratch commented 2 years ago

Hi @iwakiri0104, thanks for asking the question! Perhaps, my reply here answered your question, right?

seratch commented 2 years ago

@iwakiri0104 Relying to https://github.com/slackapi/bolt-python/issues/693#issuecomment-1266428930

It seems that you are using Socket Mode. In this case, the retry num value exists as "retry_attempt" in the body.

I've created an issue for enhancement https://github.com/slackapi/bolt-python/issues/732 to provide an easier way to access the value in future versions. Until then, please check the Java SDK implementation (https://github.com/slackapi/java-slack-sdk/pull/677) and write your own global middleware to extract the retry properties from Socket Mode payload data.

iwakiri0104 commented 2 years ago

@seratch Sorry for the misunderstanding. The reason for socket mode is for local testing, and the production environment is intended for FaaS (cloud function). What I really want to know is why the code here(#639 comment) does not allow me to see the contents of the request. I thought the above code would allow me to ignore the "X-Slack-Retry-Num", but it seems that I cannot even debug the contents of the request header in the first place.......

seratch commented 2 years ago

@iwakiri0104 Can you add the following code to your prod app? It should display all the request headers for you.

@app.middleware
def log_request_headers(logger, request, next):
    logger.info(request.headers)
    next()
iwakiri0104 commented 2 years ago

@seratch Thanks to your help, I was able to confirm the "X-Slack-Retry-Num" in the log for the first time!! I will implement duplicate prevention by referring here. Thank you very much!

seratch commented 2 years ago

@iwakiri0104 Great to hear that! Let me close this issue now.

eddyg commented 2 years ago

Out of curiosity, aren't the retries being caused by the lack of calling ack(), or am I missing something?

Shouldn't looking at X-Slack-Retry-Num (and ignoring retry messages from Slack servers because it thinks the message has not been properly delivered so it keeps sending them) be considered "bad practice"?

seratch commented 2 years ago

@eddyg Sorry for my belated response here.

In general, you're right here. In most cases, we don't recommend ignoring retried event delivery. With that being said, we are aware of some situations like "Indeed, a Slack app's Request URL couldn't respond within 3 seconds but the main logic is successfully done on the server side. So, we don't want to repeat the logic again because running it again can result in duplicated outcomes (e.g., creating the same database record twice, posting the same message in a channel)". We call it a kind of workaround but as long as a developer does understand the downside of the approach, it can be okay in the real world.