Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

บางครั้งการสร้าง Reminders สำหรับ Annual Events เป็นวิธีดีที่สุดกันไม่ให้ลืมและพลาดวันสำคัญ

ถ้าคุณและทีม/เพื่อนใช้ Slack การทำให้ Reminders อัตโนมัติผ่าน Slackbots เป็นไอเดียดี

ขณะทำเช่นนั้น หากต้องการ Slackbot ที่ Low Maintenance (ดูแลน้อย) ควรใช้ Serverless Technologies สำหรับ Concurrent Interactions กับ Source และเปิด Horizontal Scalability

เรากำลังสร้างอะไร?

เราสร้าง Event Reminder Slackbot ด้วย Python, AWS Chalice, AWS Lambda และ API Gateway สำหรับ Hosting จะให้ Users:

  • ตั้งค่า Birthdays สำหรับ Users
  • ตั้งค่า Anniversaries สำหรับ Users
  • ตั้งค่า Custom Events สำหรับ Users หรือ General Channel

หลังตั้งค่า Events แล้ว:

  • Remind ผู้คนว่า Event เฉพาะกำลังจะมาถึง ยกเว้นคนที่เป็นศูนย์กลาง Event (คนที่ถูก Mention ตอนตั้งค่า)
  • Post ไป General Channel เมื่อวันครบรอบ Event มาถึง Mention คนศูนย์กลาง (หรือ Everyone ใน Channel)

Commands

set

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

    ตั้งค่า Birthday ของ User

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

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

    ตั้งค่า Anniversary (วันเริ่มงาน) ของ User

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

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

    ตั้งค่า Custom Reminder ด้วย Message ที่ให้มา

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

get-all

  • /event get-all : แสดง Events ทั้งหมดที่ตั้งค่า

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

  • /event get-all birthday : แสดง Birthdays ทั้งหมด

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

  • /event get-all anniversary : แสดง Anniversaries ทั้งหมด

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

  • /event get-all custom : แสดง Custom Events ทั้งหมด

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

get

  • /event get birthday <user> : แสดง Birthday Details ของ User

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

  • /event get anniversary <user> : แสดง Anniversary Details ของ User

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

  • /event get custom <event_name>(can be found with get-all) : แสดง Custom Event Details

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

remove

  • /event remove birthday <user> : ลบ Birthday ของ User

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

  • /event remove anniversary <user> : ลบ Anniversary ของ User

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

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

    สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

Scheduled Reminder

  • Remind General Channel

    เมื่อเวลามาถึง Slackbot ส่ง Reminder ไป Channel ที่ระบุ สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

  • Private Message From Bot

    เมื่อเวลาลงตัว Slackbot ส่ง Private Reminder Messages สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

เครื่องมือนี้ใช้ติดตามวันสำคัญของสมาชิกทีม ช่วยรักษาความสัมพันธ์และการสื่อสารระหว่างกันให้แข็งแกร่ง

Getting Started

เตรียม Database

สร้าง Redis Database บน Upstash Console จด UPSTASH_REDIS_REST_URL และ UPSTASH_REDIS_REST_TOKEN ไว้ จะเป็น Environment Variables สำหรับ AWS

ตั้งค่า AWS Credentials

(จาก Official Chalice Repo อ้างอิงเพิ่มเติมได้ที่นั่น)

$ 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)

Conventions บางอย่าง

  • ไฟล์ .py นอก app.py ควรอยู่ใน Directory chalicelib มิฉะนั้น Import Statements อาจมีปัญหา
  • Environment Variables ทั้งหมดควร Config ใน config.json ภายใน Directory .chalice
    • Format JSON ด้วย Key: "environment_variables"

Project Source Development

  • ติดตั้ง Chalice: pip install chalice

Initiate Chalice Project

chalice new-project <project_name> สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance cd เข้าโฟลเดอร์ โปรเจกต์มาพร้อม Template

Run: chalice local ดูว่า Project ทำงานได้

app.py

Main File สำหรับ Project Structure และ Handle Slack Requests

สร้าง Project Structure และ Endpoints ตัดสินใจ Handle Events และ Schedule สำหรับ Reminder

from chalice import Chalice, Cron, Rate
import os, 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"))

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

@app.route('/', methods=["POST"], content_types=["application/x-www-form-urlencoded"])
def index():
    r = responseToDict(app.current_request.raw_body)
    headers = app.current_request.headers

    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." }

@app.schedule(Cron(0, 10, '*', '*', '?', '*'))
def periodicCheck(event):
    allKeys = getAllKeys()
    for key in allKeys:
        handleEvent(key)

def handleEvent(eventName):
    eventSplitted = eventName.split('-')
    eventType = eventSplitted[0]
    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)

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)

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)

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)

def dmEveryoneExcept(message, person):
    usersAndIds = allSlackUsers()
    for user in usersAndIds:
        if user[0] != person:
            sendDm(user[1], message)

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)

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)

allKeys = getAllKeys()
for key in allKeys:
    handleEvent(key)

chalicelib/utils.py

Main File สำหรับ Helper Functions และ Abstraction

ใช้ File นี้สำหรับ Abstractions เพื่อ Source Code ไม่ Errant และ Readability ดี

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

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

def getRealName(slackUsers, username):
    for user in slackUsers:
        if user[0] == username:
            return user[2]
    return "Nameless"

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

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

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

def postToChannel(channel, messageText):
    channelId = channelNameToId(channel)
    return postToSlack(channelId, messageText)

def sendDm(userId, messageText):
    return postToSlack(userId, messageText)

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

def responseToDict(res):
    return dict(parse_qsl(res.decode()))

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

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

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

def convertToCorrectMention(name):
    if name == "channel" or name == "here" or name == "everyone":
        return "<!{}>".format(name)
    else:
        return "<@{}>".format(name)

chalicelib/upstash.py

Main File สำหรับ Functions เกี่ยวกับ Database โดยตรง

จัดการ Database Calls Fetch, Set Key-Value Pairs Abstract Low-level Details จาก app.py เพื่อ Readability และ Modularity

Upstash Redis Database รองรับ RESTful API Calls เข้าถึง Database ได้โดยไม่ต้อง Create/Close Connections ตลอดเวลา เหมาะสำหรับ Serverless Applications

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")

def postToUpstash(parameters):
    requestURL = UPSTASH_REST_URL
    for parameter in parameters:
        requestURL += ("/" + parameter)
    resultDict = sendPostRequest(requestURL, UPSTASH_TOKEN)
    return resultDict['result']

def setEvent(parameterArray):
    postQueryParameters = ['SET']
    for parameter in parameterArray:
        parameter = parameter.split()
        for subparameter in parameter:
            postQueryParameters.append(subparameter)
    resultDict = postToUpstash(postQueryParameters)
    return resultDict

def getEvent(eventName):
    postQueryParameters = ['GET', eventName]
    date = postToUpstash(postQueryParameters)
    timeDiff = diffWithTodayFromString(date)
    totalTime = totalTimefromString(date)
    mergedDict = [date, timeDiff, totalTime]
    return mergedDict

def getAllKeys():
    return postToUpstash(['KEYS', '*'])

def removeEvent(eventName):
    postQueryParameters = ['DEL', eventName]
    resultDict = postToUpstash(postQueryParameters)
    return resultDict

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

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

Config File สำหรับ Project บน AWS

กำหนด Project Details เช่น Environment Variables และ Deployment Stages สำหรับนี้ Config Environment Variables โดยเพิ่ม:

{
    "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>"
    }
}

หลังเสร็จสิ้น

Folder Structure

Folder Structure ควรประมาณนี้:

<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>

Running Locally

Chalice รองรับ Local Deployment ทำ Development เร็วขึ้น

Run: chalice local สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

หากไม่มี Static IP ใช้ Tunnelling Service เช่น ngrok เพื่อแสดง Endpoint ให้ Slack:

./ngrok http 8000 --> Tunnels localhost:8000 สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

Configure Slack

1. ไปที่ Slack API Apps Page:

  • Create New App
    • From Scratch
    • ตั้งชื่อ App & เลือก Workspace
  • ไป OAuth & Permissions
    • Add Scopes:
      • channels:read
      • chat:write
      • chat:write.public
      • commands
      • groups:read
      • users:read
    • Install App to Workspace
      • Basic Information --> Install Your App --> Install To Workspace
  1. จด Variables (จะเป็น Env Variables สำหรับ AWS Deploy):
    • SLACK_SIGNING_SECRET:
      • Basic Information --> App Credentials --> Signing Secret
    • SLACK_BOT_TOKEN:
      • OAuth & Permissions --> Bot User OAuth Token

3. ไป Slack API Apps Page เลือก App ที่เกี่ยวข้อง:

หลัง Deploy ใช้ REST_API_URL หรือ ngrok_domain เป็น <domain>

  1. ไป Slack API Apps Page เลือก App:
  • ไป Slash Commands:
    • Create New Command:
      • Command: event
      • Request URL: <domain>
      • Config ส่วนอื่นๆ ตามต้องการ
  • หลังเปลี่ยนแปลง Slack อาจขอ Reinstall App

ยินดีด้วย!

ตอนนี้คุณมี Serverless Slackbot ที่ทำงานแล้ว Customize ได้อย่างอิสระ

พอพอใจ Local Hosting และ Results:

  • chalice deploy สำหรับ Final Deployment บน AWS Lambda และ API Gateway สร้าง Birthday Reminder Bot สำหรับ Slack ด้วย AWS Chalice และ Upstash Redis แบบ Low-Maintenance

ใช้ REST_API_URL จาก AWS Chalice ใน Slack Configurations ได้แล้ว

โปรเจกต์สมบูรณ์ดูได้ที่ Github Repo