Tập code python Slack bot đơn giản

Như bài trước đã trình bầy, kế hoạch Slack bot của tôi đang hết sức chơi vơi không biết tiến thoái thế nào. Đúng lúc đó tôi nhận được một message được gửi tự động từ Slack bot:

À, vậy là mình vẫn có duyên với vụ này. Thử hỏi trong công ty chủ nhân tạo bot trên và xin code tham khảo để học hỏi. Vậy là có được một file code Google App Script (GAS). Vốn từng tiếp xúc một thời gian với Javascript nên sau khi mày mò đọc code GAS này tôi cũng hiểu được đại khái đồng thời thông ra được vài điều:

. Nếu chỉ để “chạy”, không cần phải persist data gì cả, dù sao vẫn có cách hard code để không cần phải đọc lại kết quả chạy của lần trước

. GAS tiện ở chỗ những data thông thường có thể đưa luôn vào sheet để store thay vì phải connect DB ….

. Slack bot thực chất chỉ là 1 Slack User mà thôi. Logic mà ta muốn code không “nằm ở” hay “thuộc về” Slack bot, mà logic đó nằm ở phía sử dụng cái User mà Slack bot đại diện cho. Thực ra 1 bạn Dev. đã từng nói với tôi như vậy (Slack bot nó chỉ là User thôi) nhưng mà lúc đó tôi không có hiểu. Giờ thực tế động vào thử mới hiểu.

Tôi cũng hiểu ra vì sao bạn Dev. ấy cũng nói muốn “làm Slack bot” thì nên hiểu về Slack Api. Vậy là nhân tiện đó tôi tạm bỏ qua vấn đề python sang 1 bên và tự khám phá chút https://api.slack.com/methods?

  1. Update và Read Slack msg với ts:

Trong phiên bản GAS tham khảo đã có hàm deleteMessage() và hàm notifySlack_(message, channel, isDirectMessage), vì thế tôi dự định tự viết tiếp hàm update và read message từ Slack với GAS. Trước tiên là đọc tài liệu https://api.slack.com/methods/chat.update và tự viết hàm update. Chỉ cần sửa hàm delete là xong, nhưng còn thông tin “ts” thì làm thế nào lấy được? Tôi tự nghịch để tìm ra từ menu 3 chấm dọc thần thánh của mỗi Slack msg

.      

(về sau tôi mới biết ts là viết tắt của timestamp)

Thấy ngon ăn, tôi tiếp tục viết hàm readMsg với thông tin “ts” (tôi đoán hẳn nó là 1 dạng id của msg) và ….. lại stuck. Không có code tham khảo và cũng chưa biết phải dùng endpoint nào vì hoá ra chẳng hề có endpoint chat.read hay chat.gì đó tương tự. Hoá ra ko thể từ delete và update mà tự suy ra được read với Slack. Lại đi tìm một hồi mới ra được cái này: https://api.slack.com/methods/conversations.history, hay nhất là nó có đoạn:

Retrieving a single message

conversations.history can also be used to pluck a single message from the archive.

You’ll need a message’s ts value, uniquely identifying it within a conversation.

Sau đó là vật vã tự viết tiếp, cũng phải sửa vài lần mới chạy và lấy được đúng:

function readAMsg(chnId,tstamp) {
  var apiURL = "https://slack.com/api/conversations.history",
  jSon = getSlackAPIRequestJSON(chnId);
  jSon.method = "get";
  jSon.payload.latest = tstamp;
  jSon.payload.oldest = tstamp;
  jSon.payload.inclusive = true;
  jSon.payload.limit = 1;

  response = UrlFetchApp.fetch(apiURL, jSon);
  responseText = response.getContentText();
  data = JSON.parse(responseText);

  result = data.messages[0].text;
}

Phù, giờ thì tôi đã có thêm lựa chọn để quay lại và unstuck vấn đềbài trước

2. Persist data bằng Slack

Vâng đúng vậy. Vì mẩu data tôi cần store giữa các lần chạy code là rất nhỏ, thay vì connect vào 1 DB online gì đó để sử dụng DB, tôi hoàn toàn có thể dùng updateMsg để ghi nó vào 1 Slack msg dành riêng. Msg này có ts biết trước và cố định. Lần sau khi chạy code, tôi lại dùng ts cố định đó để đọc lấy kết quả do lần chạy trước ghi lại bằng updateMsg.

Tất cả các hàm updateMsg và readAMsg đã sẵn sàng, vì thế chỉ cần lắp ghép lại theo đúng kịch bản là được.

Rốt cuộc tôi cũng tạo được một thành quả nhỏ bé đủ để chạy đúng như ý đồ ban đầu một cách đơn giản nhất có thể, ko phải cài cắm gì:

function rotatePIC(){
  var currenPIC = readAMsg("C0UFH4XAL","1653476623.205339"); // used known ts
  var nextPIC = getNextPIC(currenPIC)
  Logger.log(nextPIC);
  updateMsg("1653476623.205339",nextPIC); // use the same ts as above for renew persisted data on Slack
  sendMsg("So let <@" + nextPIC + "> be the next cosmic traveller" ,"C0UFH4XAL");
}

Như vậy bước cuối cùng là config để đoạn code trên chạy tự động. Với GAS, việc này rất dễ, chỉ cần vào https://script.google.com/ và Add Trigger, chỉ định hàm nói trên và timing cần chạy (vd hàng tuần, hàng ngày ….)

3. Port code sang Python

Giờ thì phiên bản GAS đã có, tôi quay lại với python.

Đầu tiên, hiện đại hơn GAS 1 chút là tôi cần giấu Slack Token vào nơi kín đáo hơn, vậy là học thêm về CI/CD Variables của gitlab

và đi kèm theo là bổ sung code python để đọc ra:

import os
...
slack_api_token = os.environ['SLACK_TOKEN'] 

Ngoài ra, để có thể gọi API, trong gitlab-ci.yml tôi phải thêm thư viện requests (lưu ý đây là thư viện cơ bản, ít tính năng, để học tập là chủ yếu, nếu code cho dự án, khả năng cao bạn sẽ cần đến thư viện khác)

- pip install requests

Rồi,

và hý hửng ngồi port code sang python, và ….. những trục trặc cũng xuất hiện rất nhanh: những hàm mà tôi port từ GAS không chạy, cứ lỗi suốt.

Phát hiện ra là nếu như bên GAS chỉ cần tống các thứ vào JSON và gọi UrlFetchApp.fetch(apiURL, jSon) thì python có vẻ phức tạp hơn. Nếu như bắt chước GAS đơn giản với response = requests.post(sendApiURL, json=inJSON) thì runner cũng đơn giản luôn trả về mấy lỗi khó hiểu: missing param …. gì đó ….

Lại stuck ….!

4. Quay lại cơ bản với Postman

Hôm đó tôi lên cơ quan ngồi nên để cho nhanh tôi đã nhờ một bạn trẻ giúp đỡ luôn. Bạn hỏi: anh chạy được trên Postman chưa? Thử được trên postman thì sau đó code dễ hơn anh ạ. Hơi ớ người. Sau đó bạn ấy tìm giúp luôn cho tài liệu gọi Slack API của postman:

https://www.postman.com/slackhq/workspace/slack-api/request/13509546-d12b21a7-489e-41e3-a2b9-b220706812b3.

Theo như ở đây thì để read msg lại dùng thẳng param truyền vào URL chứ ko dùng request body gì hết!

Thế là tôi ngồi thử với Postman trên máy mình với thông tin Token, channel, ts … thật, thấy chạy được OK rồi mới quay lại python.

5. Python call API là một kiểu code khác

Sau khi thử được trên Post man,  tiếp tục tự mò mẫm tiếp ra cái hàm readMsg trên python. Cũng mất gần cả buổi chiều code đi code lại vì sai, nhầm các thứ …  Code theo cách tiếp cận này ra kết quả khác tương đối so với GAS:

tJson = {
    "token": "apiToken",
    "channel": "tbf"
}

def readMsg(targetChn,tstamp):
    readApiURL = "https://slack.com/api/conversations.history"
    tJson["token"] = slack_api_token
    tJson["channel"] = targetChn
    tJson["latest"] = tstamp
    tJson["oldest"] = tstamp
    tJson["inclusive"] = True
    tJson["limit"] = 1
    #response = requests.get(apiURL, json = tJson) this will not work.
    response = requests.get(readApiURL, params=tJson).json()["messages"]
    return response[0]["text"]

Cũng chưa phải đã xong, tôi còn phải dùng nhiều thời gian thử đi thử lại mới xong các hàm updateMsg(...) và sendMsg(...) Rốt cuộc phải kiểu này mới được (sorry code chưa optimized):

inJSON = {
    #"method": "post",
    "headers": {
        "Content-Type":"application/x-www-form-urlencoded",
        "Authorization": "Bearer ....tbf"
    },
    "payload": {
        "token":"tbf",
        "channel": "tbf", 
        "ts":"tbf",
        "text":"tbf"
    }
}

def updateMsg(targetChn,tstamp,uText):
    updateApiURL = "https://slack.com/api/chat.update"
    inJSON["payload"]["token"] = slack_api_token
    inJSON["payload"]["channel"] = targetChn
    inJSON["payload"]["ts"] = tstamp
    inJSON["payload"]["text"] = uText
    #call API
    #response = requests.post(apiURL, json=inJSON)
    response = requests.post(updateApiURL, data=inJSON["payload"],headers=inJSON["headers"] )
    return response.json()

Và cuối cùng thì mọi thứ lại chạy được với python!

Lại được enjoy chút niềm vui cày cuốc! Cảm ơn đã đọc bài và hẹn gặp lại nếu có dịp unstuck được điều gì đó!

Add a Comment

Scroll Up