Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Redis

Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

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

Nếu bạn và team/bạn bè dùng Slack, thì ý tưởng tốt là tự động hóa các nhắc nhớ này qua slackbot.

Khi làm vậy, nếu bạn muốn slackbot của bạn ít bảo trì ở chỗ; nó có thể tốt nhất dùng công nghệ serverless cho tương tác đồng thời với nguồn, cũng cho phép mở rộng ngang.

Chúng ta đang xây dựng gì

Chúng ta đang xây dựng một Event Reminder Slackbot dùng Python, AWS Chalice, AWS Lambda và API Gateway để hosting. Nó sẽ cho phép người dùng:

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

Khi sự kiện được đặt:

  • Nhắc mọi người rằng một sự kiện cụ thể sắp đến, trừ người ở trung tâm sự kiện (người được mention khi đặt sự kiện).
  • Đăng vào kênh chung khi ngày kỷ niệm sự kiện đến, mention người ở trung tâm sự kiện (hoặc mọi người trong kênh).

Các lệnh

set

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

    Đặt sinh nhật của user.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

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

    Đặt ngày kỷ niệm cho user, khi họ bắt đầu làm việc đó.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • /event set custom <YYYY-MM-DD> <user> <bất kỳ tin nhắn nào có khoảng trắng>

    Đặt nhắc nhở tùy chỉnh dùng tin nhắn cung cấp.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

get-all

  • /event get-all :

    Hiển thị tất cả sự kiện đã đặt.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • /event get-all birthday :

    Hiển thị tất cả sinh nhật đã đặt.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • /event get-all anniversary :

    Hiển thị tất cả ngày kỷ niệm đã đặt.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • /event get-all custom :

    Hiển thị tất cả sự kiện tùy chỉnh đã đặt.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

get

  • /event get birthday <user> :

    Hiển thị chi tiết sinh nhật của user.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • /event get anniversary <user> :

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

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • /event get custom <event_name>(có thể tìm với get-all) :

    Hiển thị chi tiết sự kiện tùy chỉnh dùng tin nhắn cung cấp.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

remove

  • /event remove birthday <user> :

    Xóa sinh nhật của user.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • /event remove anniversary <user> :

    Xóa ngày kỷ niệm cho user, khi họ bắt đầu làm việc đó.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • /event remove custom <event_name>(có thể tìm với get-all) :

    Xóa sự kiện tùy chỉnh dùng tin nhắn cung cấp.

    Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

Nhắc nhở theo lịch

  • Nhắc kênh chung

    Khi đến thời điểm, slackbot sẽ gửi tin nhắn nhắc nhở đến kênh chỉ định. Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

  • Tin nhắn riêng từ Bot

    Khi thời gian tiếp cận, slackbot sẽ gửi tin nhắn nhắc nhở riêng tư. Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

Vì vậy, công cụ này có thể dùng để theo dõi các ngày đặc biệt cho thành viên team. Cách này, mối quan hệ và giao tiếp giữa nhau có thể được duy trì theo 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 trên Upstash Console. Ghi nhớ UPSTASH_REDIS_REST_URL và UPSTASH_REDIS_REST_TOKEN vì chúng sẽ là biến môi trường cho AWS.

Cấu hình thông tin AWS

(Lấy từ Official Chalice Repo. Bạn có thể tham khảo thêm thông tin đó.)

$ 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ả file .py bên ngoài app.py nên đặt dưới thư mục chalicelib, nếu không import statement có thể gây vấn đề.
  • Tất cả biến môi trường nên cấu hình trong file config.json bên trong thư mục .chalice.
    • Dạng json, với key: "environment_variables"

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

  • Trước hết, vì chúng ta dùng AWS Chalice, để cài chalice:

    pip install chalice

Khởi tạo dự án Chalice

chalice new-project <project_name> Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis Sau đó, cd vào thư mục dự án. Dự án đã có sẵn template.

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

app.py

File chính cho cấu trúc dự án tổng thể và xử lý request Slack.

Với này, chúng ta tạo cấu trúc dự án và endpoint. Chúng ta quyết định cách xử lý sự kiện, lịch trình gì để 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

File chính cho hàm helper và trừu tượng hóa.

Chúng ta sẽ chủ yếu dùng file này cho trừu tượng hóa. Vì vậy, mã nguồn của chúng ta sẽ không lộn xộn và 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/upstash.py

File chính cho hàm trực tiếp liên quan đến cơ sở dữ liệu.

Ở đây, chúng ta sẽ xử lý các lệnh gọi cơ sở dữ liệu. Chúng ta sẽ lấy từ cơ sở dữ liệu, đặt cặp key-value v.v. File này cũng giúp chúng ta trừu tượng hóa chi tiết cấp thấp từ app.py, tăng khả năng đọc và tính module.

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

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

File cấu hình dự án trên AWS.

Ở đây, chúng ta định nghĩa chi tiết dự án như biến môi trường và giai đoạn triển khai. Cho này, chúng ta chỉ cấu hình biến môi trường bằng cách thêm:

{
    "environment_variables": {
        "UPSTASH_REST_URL": ,
        "UPSTASH_TOKEN": ,
        "SLACK_BOT_TOKEN": ,
        "SLACK_SIGNING_SECRET": ,
        "NOTIFY_TIME_LIMIT": ""
    }
}

Sau khi xong tất cả

Cấu trúc thư mục

Cấu trúc thư mục nên trông như thế này:

:
    app.py
    chalicelib:
        utils.py
        upstash.py
    
    .chalice:
        config.json
    

Chạy cục bộ

Chalice cho phép triển khai cục bộ, làm cho quá trình phát triển thật nhanh.

Chạy: chalice local Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

Nếu bạn không có địa chỉ IP tĩnh, bạn nên dùng dịch vụ tunneling như ngrok để show endpoint cho Slack:

./ngrok http 8000 --> Tunnels your localhost:8000 Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

Cấu hình Slack

1. Đi đến Slack API Apps Page:

  • Tạo App mới
    • From Scratch
    • Đặt tên app & chọn workspace
  • Đi đến Oauth & Permissions
    • Thêm các scope sau
      • channels:read
      • chat:write
      • chat:write.public
      • commands
      • groups:read
      • users:read
    • Cài App vào workspace
      • Basic Information --> Install Your App --> Install To Workspace
  1. Ghi nhớ các biến (Đây sẽ là env variables cho AWS deployment) :
    • SLACK_SIGNING_SECRET:
      • Đi đến Basic Information
        • App Credentials --> Signing Secret
    • SLACK_BOT_TOKEN:
      • Đi đến OAuth & Permissions
        • Bot User OAuth Token

3. Đi đến Slack API Apps Page and choose relevant app:

Sau triển khai, bạn có thể dùng REST_API_URL hoặc ngrok_domain làm <domain>.

  1. Đi đến Slack API Apps Page and choose relevant app:
  • Đi đến Slash Commands:
    • Create New Command:
      • Command : event
      • Request URL : <domain>
      • Cấu hình phần còn lại như ý thích.
  • Sau những thay đổi này, Slack có thể yêu cầu cài lại app.

Chúc mừng!

Bây giờ bạn có một Slackbot serverless hoạt động! Tự do tùy chỉnh nó như ý thích.

Sau khi bạn hài lòng với hosting cục bộ và kết quả, đơn giản:

  • chalice deploy cho triển khai cuối cùng trên AWS Lambda và API Gateway. Tạo Bot nhắc nhở sinh nhật ít bảo trì cho Slack dùng AWS Chalice & Upstash Redis

Bây giờ bạn có thể dùng REST_API_URL do AWS Chalice cung cấp trên cấu hình Slack của bạn.

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