Computer >> Máy Tính >  >> Lập trình >> Redis

Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

Đôi khi, cách tốt nhất là tạo lời nhắc cho các sự kiện hàng năm của bạn để bạn không quên và bỏ lỡ những ngày đặc biệt đó.

Nếu bạn và nhóm / bạn bè của bạn đang sử dụng Slack, thì bạn nên tự động hóa những lời nhắc này thông qua slackbots.

Trong khi làm như vậy, nếu bạn muốn slackbot của mình là một công cụ bảo trì thấp trong đó; tốt nhất có thể là sử dụng công nghệ không máy chủ cho các tương tác đồng thời với nguồn, đồng thời cho phép khả năng mở rộng theo chiều ngang.

Những gì chúng tôi đang xây dựng

Chúng tôi đang xây dựng Slackbot nhắc nhở sự kiện sử dụng Python, AWS Chalice, AWS Lambda và API Gateway để lưu trữ. Nó sẽ cho phép người dùng:

  • Đặt sinh nhật cho người dùng.
  • Đặt các ngày kỷ niệm cho người dùng.
  • Đặt các sự kiện tùy chỉnh cho người dùng hoặc kênh chung

Sau khi các sự kiện được thiết lập:

  • Nhắc mọi người rằng một sự kiện cụ thể sắp diễn ra, ngoại trừ người ở trung tâm của sự kiện (người đã được đề cập khi thiết lập sự kiện).
  • Bài đăng lên kênh chung sau khi kỷ niệm sự kiện đến, đề cập đến người ở trung tâm của sự kiện (hoặc tất cả mọi người trong kênh).

Lệnh

set

  • /event set birthday <YYYY-MM-DD> <user>

    Đặt ngày sinh của người dùng.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event set anniversary <YYYY-MM-DD> <user>

    Đặt ngày kỷ niệm cho người dùng, khi họ bắt đầu làm việc ở đó.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event set custom <YYYY-MM-DD> <user> <any kind of message with whitespaces>

    Đặt lời nhắc tùy chỉnh bằng tin nhắn được cung cấp.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

get-all

  • /event get-all :

    Hiển thị tất cả các sự kiện đã được thiết lập.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event get-all birthday :

    Hiển thị tất cả các ngày sinh đã được đặt.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event get-all anniversary :

    Hiển thị tất cả các ngày kỷ niệm đã được thiết lập.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event get-all custom :

    Hiển thị tất cả các sự kiện tùy chỉnh đã được thiết lập.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

lấy

  • /event get birthday <user> :

    Hiển thị chi tiết ngày sinh của người dùng.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event get anniversary <user> :

    Hiển thị chi tiết ngày kỷ niệm cho người dùng, khi họ bắt đầu làm việc ở đó.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event get custom <event_name>(can be found with get-all) :

    Hiển thị chi tiết sự kiện tùy chỉnh bằng cách sử dụng thông báo được cung cấp.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

xóa

  • /event remove birthday <user> :

    Xóa ngày sinh của người dùng.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event remove anniversary <user> :

    Xóa ngày kỷ niệm cho người dùng, khi họ bắt đầu làm việc ở đó.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • /event remove custom <event_name>(can be found with get-all) :

    Xóa sự kiện tùy chỉnh bằng cách sử dụng thông báo được cung cấp.

    Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

Lời nhắc đã lên lịch

  • Nhắc kênh chung

    Khi đến thời điểm, slackbot sẽ gửi thông báo nhắc nhở đến kênh được chỉ định. Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

  • Tin nhắn riêng từ Bot

    Khi thời gian đến gần, slackbot sẽ gửi tin nhắn nhắc nhở riêng tư. Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

Vì vậy, công cụ này có thể được sử dụng để theo dõi các ngày đặc biệt của các thành viên trong nhóm. Bằng cách này, các mối quan hệ và giao tiếp giữa các bên có thể được duy trì một cách lành mạnh.

Bắt đầu

Chuẩn bị Cơ sở dữ liệu

Chúng ta có thể tạo cơ sở dữ liệu Redis của mình trên Upstash Console. Lưu ý UPSTASH_REDIS_REST_URL và UPSTASH_REDIS_REST_TOKEN vì chúng sẽ là các biến môi trường cho AWS.

Định cấu hình Thông tin đăng nhập AWS

(Lấy từ đại diện chính thức của Chalice. Bạn có thể tham khảo thêm thông tin tại đó.)

$ mkdir ~/.aws
$ cat >> ~/.aws/config
[default]
aws_access_key_id=YOUR_ACCESS_KEY_HERE
aws_secret_access_key=YOUR_SECRET_ACCESS_KEY
region=YOUR_REGION (such as us-west-2, us-west-1, etc)

Một số Quy ước

  • Tất cả .py các tệp bên ngoài app.py nên được đặt dưới chalicelib thư mục, nếu không câu lệnh nhập có thể gây ra sự cố.
  • Tất cả các biến môi trường phải được định cấu hình trong config.json tệp bên trong .chalice thư mục.
    • Ở định dạng json, với khóa:"environment_variables"

Phát triển nguồn dự án

  • Trước hết, vì chúng tôi đang sử dụng AWS Chalice , để cài đặt chalice:

    pip install chalice

Bắt đầu Dự án Chalice

chalice new-project <project_name> Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis Sau đó, cd vào thư mục dự án. Dự án alreadys đi kèm với một mẫu.

Chạy:chalice local để thấy rằng dự án hoạt động.

app.py

Tệp chính cho cấu trúc dự án tổng thể và để xử lý các yêu cầu Slack.

Với điều này, chúng tôi tạo cấu trúc dự án và các điểm cuối của chúng tôi. Chúng tôi quyết định cách xử lý các sự kiện, những gì cần lên lịch để nhắc nhở hoạt động.

from chalice import Chalice, Cron, Rate
import os
import random
from datetime import date
from chalicelib.utils import responseToDict, postToChannel, diffWithTodayFromString, allSlackUsers, sendDm, validateRequest, convertToCorrectMention
from chalicelib.upstash import setHandler, getAllHandler, getEvent, getAllKeys, removeEvent

app = Chalice(app_name='birthday-slackbot')
NOTIFY_TIME_LIMIT = int(os.getenv("NOTIFY_TIME_LIMIT"))


# Sample route for get requests.
@app.route('/', methods=["GET"])
def something():
    return {
        "Hello": "World"
        }

# Configuring POST request endpoint.
# Command is parsed and handled/directed to handler
@app.route('/', methods=["POST"], content_types=["application/x-www-form-urlencoded"])
def index():

    # Parse the body for ease of use
    r = responseToDict(app.current_request.raw_body)
    headers = app.current_request.headers

    # Check validity of the request.
    if not validateRequest(headers, r):
        return {"Status": "Validation failed."}


    commandArray = r['text'].split()
    command = commandArray.pop(0)

    try:
        if command == "set":
            setHandler(commandArray)
            return {
            'response_type': "ephemeral",
            'text': "Set the event."
            }

        elif command == "get":
            eventType = commandArray[0]
            eventName = eventType + "-" + commandArray[1]
            resultDict = getEvent(eventName)
            return {
            'response_type': "ephemeral",
            'text': "`{}` Details:\n\n Date: {}\nRemaining: {} days!".format(eventName, resultDict[0], resultDict[1])
            }

        elif command == "get-all":

            stringResult = getAllHandler(commandArray)
            return {
            'response_type': "ephemeral",
            'text': "{}".format(stringResult)
            }

        elif command == "remove":
            eventName = "{}-{}".format(commandArray[0], commandArray[1])
            removeEvent(eventName)
            return {
            'response_type': "ephemeral",
            'text': "Removed the event."
            }
        else:
            return {
            'response_type': "ephemeral",
            'text': "Wrong usage of the command."
            }
    except:
        print("some stuff")
        return {
            'response_type': "ephemeral",
            'text': "Some problem occured. Please check your command."
        }


# Run at 10:00 am (UTC) every day.
@app.schedule(Cron(0, 10, '*', '*', '?', '*'))
def periodicCheck(event):
    allKeys = getAllKeys()
    for key in allKeys:
        handleEvent(key)


# Generic event is parsed and directed to relevant handlers.
def handleEvent(eventName):
    eventSplitted = eventName.split('-')

    eventType = eventSplitted[0]

    # discard @ or ! as a first character
    personName = eventSplitted[1][1:]
    personMention = convertToCorrectMention(personName)

    eventDict = getEvent(eventName)
    remainingDays = eventDict[1]
    totalTime = eventDict[2]


    if eventType == "birthday":
        birthdayHandler(personMention, personName, remainingDays)

    elif eventType == "anniversary":
        anniversaryHandler(personMention, personName, remainingDays, totalTime)

    elif eventType == "custom":
        eventMessage = "Not specified"
        if len(eventSplitted) == 3:
            eventMessage = eventSplitted[2]
        customHandler(eventMessage, personMention, personName, remainingDays)

# Handles birthday events.
def birthdayHandler(personMention, personName, remainingDays):
    if remainingDays == 0:
        sendRandomBirthdayToChannel('general', personMention)
    if remainingDays <= NOTIFY_TIME_LIMIT:
        dmEveryoneExcept("{} day(s) until {}'s birthday!".format(remainingDays, personMention), personName)

# Handles anniversary events.
def anniversaryHandler(personMention, personName, remainingDays, totalTime):
    if remainingDays == 0:
        sendRandomAnniversaryToChannel('general', personMention, totalTime)
    if remainingDays <= NOTIFY_TIME_LIMIT:
        dmEveryoneExcept("{} day(s) until {}'s anniversary! It will be {} year(s) since they joined!".format(remainingDays, personMention, totalTime), personName)

# Handles custom events.
def customHandler(eventMessage, personMention, personName, remainingDays):
    if remainingDays == 0:
        postToChannel('general', "`{}` is here {}!".format(eventMessage, personMention))
    elif remainingDays <= NOTIFY_TIME_LIMIT:
        dmEveryoneExcept("{} day(s) until {} `{}`!".format(remainingDays, personMention, eventMessage), personName)


# Sends private message to everyone except for the person given.
def dmEveryoneExcept(message, person):
    usersAndIds = allSlackUsers()
    for user in usersAndIds:
        if user[0] != person:
            sendDm(user[1], message)


# Sends randomly chosen birthday message to specified channel.
def sendRandomBirthdayToChannel(channel, personMention):
    messageList = [
        "Happy Birthday {}! Wishing you the best!".format(personMention),
        "Happy Birthday {}! Wishing you a happy age!".format(personMention),
        "Happy Birthday {}! Wishing you a healthy, happy life!".format(personMention),
    ]
    message = random.choice(messageList)
    return postToChannel('general', message)

# Sends randomly chosen anniversary message to specified channel.
def sendRandomAnniversaryToChannel(channel, personMention, totalTime):
    messageList = [
        "Today is the anniversary of {} joining! It has been {} years since they joined!".format(personMention, totalTime - 1),
        "Celebrating the anniversary of {} joining! It has been {} years!".format(personMention, totalTime - 1),
        "Congratulating {} for entering {}(th) year here!".format(personMention, totalTime),
    ]
    message = random.choice(messageList)
    return postToChannel('general', message)


# We want to run our event handlers when the project is deployed/redeployed.
allKeys = getAllKeys()
for key in allKeys:
    handleEvent(key)

chalicelib / utils.py

Tệp chính cho các hàm trợ giúp và sự trừu tượng.

Chúng tôi sẽ chủ yếu sử dụng tệp này cho các phần tóm tắt. Vì vậy, mã nguồn của chúng tôi sẽ không lộn xộn và sẽ duy trì khả năng đọc.

from urllib import request
import urllib
from urllib.parse import parse_qsl
import json
import os
import hmac
import hashlib
from datetime import date


SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN")
SLACK_SIGNING_SECRET = os.getenv("SLACK_SIGNING_SECRET")

# Returns real name of the slack user.
def getRealName(slackUsers, username):
    for user in slackUsers:
        if user[0] == username:
            return user[2]
    return "Nameless"

# Returns all slack users in the workspace.
def allSlackUsers():
    resultDict = sendPostRequest("https://slack.com/api/users.list", SLACK_BOT_TOKEN)
    members = resultDict['members']

    userMembers = []
    for member in members:
        if not member['deleted'] and not member['is_bot']:
            userMembers.append([member['name'], member['id'], member['real_name']])

    return userMembers

# Returns the id of the given channel.
def channelNameToId(channelName) :
    resultDict = sendPostRequest("https://slack.com/api/conversations.list", SLACK_BOT_TOKEN)
    for channel in resultDict['channels']:
        if (channel['name'] == channelName):
            return channel['id']
    return None

# Posts to given slack channelId with given message.
def postToSlack(channelId, messageText):
    data = {
        "channel": channelId,
        "text": messageText
    }
    data = json.dumps(data)
    data = str(data)
    data = data.encode('utf-8')
    resultDict = sendPostRequest("https://slack.com/api/chat.postMessage", SLACK_BOT_TOKEN, data)
    return resultDict

# Posts to a slack channel.
def postToChannel(channel, messageText):
    channelId = channelNameToId(channel)
    return postToSlack(channelId, messageText)

# Sends a private message to a user with userId.
def sendDm(userId, messageText):
    return postToSlack(userId, messageText)

# Sends generic post request and returns the result.
def sendPostRequest(requestURL, bearerToken, data={}):
    req = request.Request(requestURL, method="POST", data=data)
    req.add_header("Authorization", "Bearer {}".format(bearerToken))
    req.add_header("Content-Type", "application/json; charset=utf-8")

    r = request.urlopen(req)
    resultDict = json.loads(r.read().decode())
    return resultDict

# Parses and converts the res to dict.
def responseToDict(res):
    return dict(parse_qsl(res.decode()))


# Dates are given as: YYYY-MM-DD
# Returns difference between current day and the anniversary.
def diffWithTodayFromString(dateString):
    now = date.today()
    currentYear = now.year

    dateTokens = dateString.split("-")
    month = int(dateTokens[1])
    day = int(dateTokens[2])

    if now > date(currentYear, month, day):
        return (date((currentYear + 1), month, day) - now).days
    return (date(currentYear, month, day) - now).days


# Dates are given as: YYYY-MM-DD
# Calculates the total time that has passed until current date.
def totalTimefromString(dateString):
    now = date.today()

    dateTokens = dateString.split("-")
    year = int(dateTokens[0])
    month = int(dateTokens[1])
    day = int(dateTokens[2])

    then = date(year, month, day)

    years = now.year - then.year
    return years + 1

# Validate requests coming to endpoint.
# Hashes request body with timestamp and signing secret.
# Then, compares that hash with slack signature.
def validateRequest(header, body):

    bodyAsString = urllib.parse.urlencode(body)

    timestamp = header['x-slack-request-timestamp']
    slackSignature = header['x-slack-signature']
    baseString = "v0:{}:{}".format(timestamp, bodyAsString)

    h =  hmac.new(SLACK_SIGNING_SECRET.encode(), baseString.encode(), hashlib.sha256)
    hashResult = h.hexdigest()
    mySignature = "v0=" + hashResult

    return mySignature == slackSignature

# Converts given name to mention string.
def convertToCorrectMention(name):
    if name == "channel" or name == "here" or name == "everyone":
        return "<!{}>".format(name)
    else:
        return "<@{}>".format(name)

chalicelib / upash.py

Tệp chính cho các chức năng liên quan trực tiếp đến cơ sở dữ liệu.

Ở đây, chúng tôi sẽ xử lý các cuộc gọi cơ sở dữ liệu của chúng tôi. Chúng tôi sẽ tìm nạp từ cơ sở dữ liệu, đặt các cặp khóa-giá trị, v.v. Tệp này cũng giúp chúng tôi tóm tắt các chi tiết cấp thấp từ app.py , nâng cao khả năng đọc và tính mô-đun.

Một điều tuyệt vời về Cơ sở dữ liệu Upstash Redis là nó hỗ trợ các lệnh gọi API RESTFUL. Bằng cách này, bạn có thể truy cập cơ sở dữ liệu của mình mà không cần phải liên tục tạo và đóng các kết nối, đây là cách để áp dụng cho các ứng dụng không có máy chủ.

from chalicelib.utils import sendPostRequest, getRealName, allSlackUsers, diffWithTodayFromString, totalTimefromString
import os

UPSTASH_REST_URL = os.getenv("UPSTASH_REST_URL")
UPSTASH_TOKEN = os.getenv("UPSTASH_TOKEN")

# Posts to Upstash Rest Url with parameters given.
def postToUpstash(parameters):
    requestURL = UPSTASH_REST_URL
    for parameter in parameters:
        requestURL += ("/" + parameter)

    resultDict = sendPostRequest(requestURL, UPSTASH_TOKEN)
    return resultDict['result']


# Sets key-value pair for the event with given parameters.
def setEvent(parameterArray):

    postQueryParameters = ['SET']

    for parameter in parameterArray:
        parameter = parameter.split()
        for subparameter in parameter:
            postQueryParameters.append(subparameter)

    resultDict = postToUpstash(postQueryParameters)

    return resultDict


# Returns event details from the event given.
def getEvent(eventName):
    postQueryParameters = ['GET', eventName]
    date = postToUpstash(postQueryParameters)

    timeDiff = diffWithTodayFromString(date)
    totalTime = totalTimefromString(date)
    mergedDict = [date, timeDiff, totalTime]
    return mergedDict

# Fetches all keys (events) from the database
def getAllKeys():
    return postToUpstash(['KEYS', '*'])

# Deletes given event from the database.
def removeEvent(eventName):
    postQueryParameters = ['DEL', eventName]
    resultDict = postToUpstash(postQueryParameters)
    return resultDict


# Handles set request by parsing and configuring setEvent function parameters.
def setHandler(commandArray):
    eventType = commandArray.pop(0)
    date = commandArray.pop(0)
    user = commandArray.pop(0)

    if eventType == "birthday":
        listName = "birthday-" + user
        return setEvent( [listName, date] )

    elif eventType == "anniversary":
        listName = "anniversary-" + user
        return setEvent( [listName, date] )

    elif eventType == "custom":
        message = ""
        for string in commandArray:
            message += string + "_"

        listName = "custom-" + user + "-" + message
        user = commandArray[1]
        return setEvent( [listName, date] )
    else:
        return

# Handles get-all requests.
def getAllHandler(commandArray):
    filterParameter = None
    if len(commandArray) == 1:
        filterParameter = commandArray[0]

    allKeys = getAllKeys()
    birthdays = []
    anniversaries = []
    customs = []

    slackUsers = allSlackUsers()

    stringResult = "\n"
    for key in allKeys:
        if key[0] == 'b':
            birthdays.append(key)
        elif key[0] == 'a':
            anniversaries.append(key)
        elif key[0] == 'c':
            customs.append(key)

    if filterParameter is None or filterParameter == "birthday":
        stringResult += "Birthdays:\n"
        for bday in birthdays:
            tag = bday.split('-')[1]
            username = tag[1:]
            realName = getRealName(slackUsers, username)
            details = getEvent(bday)

            stringResult += "`{}` ({}): {} - `{} days` remaining!\n".format(tag, realName, details[0], details[1])

    if filterParameter is None or filterParameter == "anniversary":
        stringResult += "\nAnniversaries:\n"
        for ann in anniversaries:
            tag = ann.split('-')[1]
            username = tag[1:]
            realName = getRealName(slackUsers, username)
            details = getEvent(ann)

            stringResult += "`{}` ({}): {} - `{} days` remaining!\n".format(tag, realName, details[0], details[1])

    if filterParameter is None or filterParameter == "custom":
        stringResult += "\nCustom Reminders:\n"
        for cstm in customs:
            splitted = cstm.split('-')
            username = splitted[2]
            realName = getRealName(slackUsers, username)
            details = getEvent(cstm)

            stringResult += "`{}-{}` ({}): {}\n".format(splitted[1], splitted[2], getRealName(slackUsers, username), details[0])

    return stringResult

. chalice / config.json

Tệp cấu hình của dự án trên AWS.

Ở đây, chúng tôi xác định chi tiết dự án của chúng tôi như các biến môi trường và các giai đoạn triển khai. Đối với điều này, chúng tôi sẽ chỉ định cấu hình các biến môi trường bằng cách thêm:

{
  "environment_variables": {
    "UPSTASH_REST_URL": <UPSTASH_REDIS_REST_URL>,
    "UPSTASH_TOKEN": <UPSTASH_REDIS_REST_TOKEN>,
    "SLACK_BOT_TOKEN": <SLACK_BOT_TOKEN>,
    "SLACK_SIGNING_SECRET": <SLACK_SIGNING_SECRET>,
    "NOTIFY_TIME_LIMIT": "<amount of days before getting notifications for events>"
    }
}

Sau khi tất cả đã xong

Cấu trúc thư mục

Cấu trúc thư mục của bạn sẽ giống như sau:

<project_name>:
    app.py

    chalicelib:
        utils.py
        upstash.py
        <Some other default files generated by chalice>

    .chalice:
        config.json
        <Some other default files generated by chalice>

Chạy cục bộ

Chalice cho phép triển khai cục bộ, giúp quá trình phát triển thực sự nhanh chóng.

Chạy:chalice local Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

Nếu bạn không có địa chỉ IP tĩnh, thì bạn nên sử dụng dịch vụ đường hầm như ngrok để bạn có thể hiển thị điểm cuối của mình cho Slack:

./ngrok http 8000 -> Đường hầm cho máy chủ cục bộ của bạn:8000 Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

Định cấu hình Slack

1. Truy cập Trang ứng dụng API Slack:

  • Tạo ứng dụng mới
    • Từ Scratch
    • Đặt tên cho ứng dụng của bạn và chọn một không gian làm việc
  • Đi tới Oauth &Permissions
    • Thêm các phạm vi sau
      • kênh:đọc
      • trò chuyện:viết
      • trò chuyện:write.public
      • lệnh
      • nhóm:đọc
      • người dùng:đọc
    • Cài đặt Ứng dụng vào không gian làm việc
      • Thông tin cơ bản -> Cài đặt ứng dụng của bạn -> Cài đặt vào không gian làm việc
  1. Lưu ý các biến (Đây sẽ là các biến env để triển khai AWS):
    • SLACK_SIGNING_SECRET :
      • Đi tới Thông tin Cơ bản
        • Thông tin đăng nhập ứng dụng -> Bí mật ký kết
    • SLACK_BOT_TOKEN :
      • Đi tới OAuth &Permissions
        • Mã thông báo OAuth của người dùng Bot

Sau khi triển khai, bạn có thể sử dụng REST_API_URL hoặc ngrok_domain dưới dạng <domain> .

  1. Truy cập Trang ứng dụng API của Slack và chọn ứng dụng có liên quan:
  • Đi tới Lệnh Slash:
    • Tạo lệnh mới:
      • Lệnh:event
      • URL yêu cầu:<domain>
      • Định cấu hình phần còn lại theo cách bạn muốn.
  • Sau những thay đổi này, Slack có thể yêu cầu cài đặt lại ứng dụng.

Xin chúc mừng!

Bây giờ bạn có một Slackbot không máy chủ đang hoạt động! Hãy tùy chỉnh nó theo cách bạn muốn.

Sau khi bạn hài lòng với kết quả và lưu trữ cục bộ, chỉ cần:

  • chalice deploy để triển khai lần cuối trên AWS Lambda và API Gateway. Slackbot sinh nhật không máy chủ với AWS Chalice và Upstash Redis

Giờ đây, bạn có thể sử dụng REST_API_URL do AWS Chalice cung cấp trên các cấu hình Slack của mình.

Để có dự án hoàn chỉnh, bạn có thể truy cập Github Repo.