目錄

[Bot系列 1] 在 GCP 上面建立一個自己的 Telegram Bot

前言

如果你想要在 GCP 上建立一個免費的 Telegram Bot,需要準備以下幾個東西:

  1. GCP VM
  2. 固定的 IP 位址
  3. Flask
  4. nginx、ngrok

因為 Telegram 傳送給 Bot 的訊息需要走 HTTPS,所以還需要擁有自己的網域和 SSL 憑證。在這部份,我們可以透過使用 nip.io 以及 certbot 來達成。

1. GCP

在 GCP 上,每個 Google 帳號都可以免費試用 90 天或 300 美元的額度,因此我們可以在 GCP 上建立 VM 或 Kubernetes 而不需要花費任何費用。

首先,我們需要在 GCP 上建立一個 VM 作為我們的開發環境,同時作為 Bot 的執行地方,並使用 Ubuntu 20.04 作為軟體平台。在建立 VM 後,VM 的外部 IP 位址預設是臨時的,這意味著每當 VM 重啟時 IP 位址可能會更改。這會導致每次都需要重新設定 Webhook 的位置。因此,我們可以按照下圖所示的方式進行修改。

進去之後就依照妳想要的做填寫,“連接到:” 的部分則選擇妳剛剛創建的 VM ,最後就可以看到如第一張圖一樣 “外部 IP 位址” 已經變成固定的。

到這邊為止我們的1、2步驟就完成了。接下來就會透過 Flask 來建立我們 Bot 的 API。

2. Flask and Telegram bot

2.1 Flask 是什麼

Flask 是一個輕量級的 Python Web 框架,它的目標是簡單易用。它提供了基本的 Web 開發工具和一些可擴展的插件,可以幫助開發者快速構建 Web 應用程序。可以通過很短的程式就運行起來,下面為一個最簡單的範例,並通過 python 指令執行即可啟動。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# flask_test.py

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello, World!'

@app.route('/about')
def about():
    return render_template('about.html')

if __name__ == '__main__':
    app.run()
     
  • / 路由 ( 根目錄 ),當用戶訪問應用程序的根路徑時,會顯示 “Hello, World !”
  • /about 路由,當用戶訪問 /about 時,會顯示一個渲染的 HTML 頁面,該頁面可以在 templates 文件夾中找到。

由於 Flask 預設是使用 port 5000,所以只需要輸入 http://127.0.0.1:5000 就可以看到 “Hello, World !” 的頁面。

2.2 Telegram bot

在開始寫程式之前我們會需要先創建一個自己的 telegram bot,如何創建機器人並且取得 Bot 的 access token 的部分網路資源都蠻多的就不多敘述。

這邊我們會使用 python 中的 python-telegram-bot 套件來運行我們的 Bot 程式,我安裝的版本號為 13.15。可以通過下面的指令進行安裝

1
pip install python-telegram-bot==13.15

安裝好之後就可以開始寫一個簡單的範例程式,其中 config.ini 可以用來記錄 Tokenwebhook url,這樣會比較方便管理。

1
2
3
4
5
# config.ini

[TELEGRAM]
ACCESS_TOKEN = <Telegram Bot Token>
WEBHOOK_URL = <Webhook Url>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# main.py

import logging
import configparser
from flask import Flask, request

import requests
import telegram
from telegram import Update
from telegram.ext import Updater, Filters, CallbackContext
from telegram.ext import MessageHandler, CommandHandler

# Load data from config.ini file
config = configparser.ConfigParser()
config.read('config.ini')

# 設定 bot token 和 webhook URL
TOKEN = config['TELEGRAM']['ACCESS_TOKEN']
WEBHOOK_URL = config['TELEGRAM']['WEBHOOK_URL']

# 之後用來設定 Bot 的 webhook
Info_Webhook_URL = f"https://api.telegram.org/bot{TOKEN}/getWebhookInfo"
Set_Webhook_URL = f"https://api.telegram.org/bot{TOKEN}/setWebhook?url={WEBHOOK_URL}"
Delete_WebhookInfo_URL = f"https://api.telegram.org/bot{TOKEN}/deleteWebhook"


# 創建 Flask 應用程序對象
app = Flask(__name__)
logging.basicConfig(level=logging.DEBUG)

# 定義 /start 指令處理函式
def start(update: Update, context: CallbackContext):
    context.bot.send_message(
        chat_id=update.message.chat.id, text="歡迎使用本機器人!")  

def reply_handler(update: Update, context: CallbackContext):
    """Reply message."""
    if update.message:
        context.bot.send_message(chat_id=update.message.chat.id, text=update.message.text)


# 接收其他程式要傳遞的訊息
@app.route("/Send_Message", methods=['POST'])
def Send_Message():
    msg = request.values.get("Message")
    chat_ID = request.form.get("Chat_ID")
    # print(f"msg = {msg}, chat_ID = {chat_ID}")

    url = f'https://api.telegram.org/bot{TOKEN}/sendMessage?chat_id={chat_ID}&text={msg}'
    requests.post(url)
    
    return f"msg = {msg}, chat_ID = {chat_ID}"

# 定義 webhook 接收請求的路由
@app.route('/callback', methods=['POST'])
def webhook():
    # 從 POST 請求中解析更新
    update = telegram.Update.de_json(request.get_json(force=True), updater.bot)
   
	# 將更新傳遞給調度器進行處理
    updater.dispatcher.process_update(update)
    
    # 回應 OK 狀態碼
    return 'OK'

  

updater = Updater(TOKEN)
updater.dispatcher.add_handler(MessageHandler(Filters.text & ~Filters.command, reply_handler))
updater.dispatcher.add_handler(CommandHandler("start", callback=start))

if __name__ == "__main__":
    r = requests.get(Delete_WebhookInfo_URL)
    r = requests.get(Set_Webhook_URL)
    r = requests.get(Info_Webhook_URL)
    print(r.text)

    app.run(debug=True, port=5000)

這邊 Line 43 的Send_Message()功能是可以接收其他程式傳送過來的訊息後再傳遞給使用者,其他程式傳遞給 Bot 要傳遞的訊息部分可以寫成這樣,Chat_ID可以直接先輸入自己的 ID,未來要給其他人使用的時候再寫成動態的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# AnotherFile.py

def Send_message_to_user(Message):
    url = 'http://localhost:5000/Send_Message'
    obj = {'Message': Message,
            'Chat_ID': "xxxxx"}
    
    x = requests.post(url, data = obj)

    print(x.text)

Tip

add_handler的順序會影響訊息的結果。例如後續可能會使用到的 ConversationHandler 如果放在 MessageHandler 之後,就有可能造成需要傳遞給 ConversationHandler 的資料被 MessageHandler 搶走。以下這兩種有可能產生不同的結果。

1
2
3
4
5
6
7
updater.dispatcher.add_handler(Conversation.conv_handler)
updater.dispatcher.add_handler(MessageHandler(~Filters.command, reply_handler))

# OR

updater.dispatcher.add_handler(MessageHandler(~Filters.command, reply_handler))
updater.dispatcher.add_handler(Conversation.conv_handler)

其中可以 57、58 行的 MessageHandlerCommandHandler,它們是主要用於處理不同種類訊息的處理器。

  • CommandHandler : 處理命令訊息的處理器,當用戶在聊天中輸入以 / 開頭的文字,Bot 會收到一個命令訊息,例如 /start。CommandHandler 可以根據不同的命令,執行不同的操作,例如回覆用戶、儲存用戶數據、發送響應訊息等。

  • MessageHandler : 是處理普通訊息的處理器,當用戶在聊天中發送文字、圖片、語音、影片等訊息時,Bot 會收到一個普通訊息。MessageHandler 可以根據不同的訊息類型,執行不同的操作,例如回覆用戶、儲存用戶數據、發送響應訊息等。

其中可以看到 MessageHandler 中的 Filters.text~Filters.command,這邊主要是用來進一步篩選使用者傳送的資訊。

  • Filters.text : 代表純文字的訊息可以接收。
  • ~Filters.command : 代表不接受指令相關的訊息 ( 例如: /start )。如果想要他也接收的話可以把前面的 "~" 去除掉。

2.3 ngrok 測試

現在我們已經有了一個簡易的 Telegram Bot 的程式了,接者就是要來測試這個程式是否可以順利運行。我們可以透過 ngrok 這個軟體來進行測試。ngrok 是一款開源的反向代理軟體,它可以將本地應用程序公開到公共網路。讓大家通過它提供的網址讓外部流量轉發到本地應用程式上。

首先,先到官網上下載 ngrok 。下載好之後執行下方指令後,就可以看到下圖中紅色方框內的網址就是可以讓外部設備連線到本地應用的 URL。

1
ngrok http 5000

將這個 URL 複製並添加到 2.2 中創建的 config.ini 檔案中的 WEBHOOK_URL 中。但是最後還必須要加上 /callback 才可以,這是因為設定 flask 的時候所設定的。所以最後的 config.ini 就會長這樣。這樣,應用程式就可以使用 ngrok 提供的 URL 來接收來自 Telegram 伺服器的 Webhook 請求了。

1
2
3
4
5
# config.ini

[TELEGRAM]
ACCESS_TOKEN = <Telegram Bot Token>
WEBHOOK_URL = https://3cbe-114-24-61-19.jp.ngrok.io/callback

但是因為這個只是為了方便做程式上的測試使用,而且 ngrok 所提供的 URL 也是有時間限制的。所以我們接下來會透過 nip.iocertbot 建立一個自己的公開網址。

3. Nginx 和 公開網址

由於 Telegram 伺服器所傳送過來資料都必須要採用 https 的方式,所以我們必須要註冊一個 SSL 的憑證來達到 https 的功能

3.1 Overview

nip.io

Nip.io 是一個特殊的 DNS 服務,它可以讓我們使用 IP 地址作為域名來訪問網站,而無需設置本地 DNS。例如,IP 地址為 10.0.0.1,就可以使用 10.0.0.1.nip.io 這樣的域名來訪問網站。

certbot

Certbot 是一個免費、開源的工具,它可以簡化 HTTPS 通信的部署過程,並且提供自動化的 SSL/TLS 憑證管理和更新。Certbot 可以自動化地向 Let’s Encrypt 獲取 SSL/TLS 憑證,並且自動部署在您的網站上,從而實現簡化且安全的 HTTPS 部署。

Nginx

Nginx 是一個高性能的、開源的網頁伺服器和反向代理服務器。它被廣泛地應用於網站和應用程式的部署中,並且在處理高流量和低延遲的網路應用程式時表現出色。

Nginx 支持多種網頁服務器、反向代理和負載平衡的功能,例如靜態文件服務、動態內容的處理、HTTP/2 協議、SSL/TLS 加密、IP 限制、基於 URI 的路由、緩存、安全性等等。Nginx 還具有高可擴展性和高可定制性,它可以通過模組化的方式擴展和定制其功能,並且可以輕鬆地整合到現有的系統中。

在實際生產環境中 nginx 會比 ngrok 來的要好及穩定。

3.2 流程

這邊的流程我們透過 nip.io 和 certbot 搞定 https 之後。將我們的 Bot 透過 Flask 運行在本地端,接者將 nginx 作為我們對外的接口,當外部流量進來的時候可以透過 nginx 轉發到 bot 上面。

3.3 Nginx 安裝 & SSL 申請

3.3.1 Nginx 安裝

在 GCP 的 VM 中透過下面的指令安裝 nginx。安裝完成之後在瀏覽器輸入 http://<VM_IP> 就可以看到 nginx 成功執行的畫面

1
2
sudo apt-get install nginx
sudo systemctl start nginx

檢查 Nginx 配置檔案是否正確,/etc/nginx/nginx.conf

1
sudo nginx -t

3.3.2 SSL 申請

我們可以透過上面提到的 Certbot 進行申請SSL,根據自己的系統進行選擇,這邊我是選擇 nginxUbuntu 20

Certbot 需要使用到snapd。確認是否有安裝到 snapd。

1
snap version

確保 snapd 是最新版

1
sudo snap install core; sudo snap refresh core

安裝 Certbot

1
sudo snap install --classic certbot

確保 Certbot 指令可以運作

1
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Run Certbot,並且讓他自動幫你修改 nginx 的設定檔

1
sudo certbot --nginx

過程中會要輸入一些資料,其中 domain 的部分就輸入 <VM_IP>.nip.io 即可,或是本身就有其他的 domain 也可以用在這邊

檢查自動訂閱 SSL

1
sudo certbot renew --dry-run

檢查 nginx 設定檔是否有錯誤並重啟

1
2
3
4
5
sudo nginx -t
sudo systemctl reload nginx

# 如果有需要再執行
sudo systemctl reload nginx

現在再通過 https://<VM_IP>.nip.io 應該就可以看到 nginx 成功的畫面了,如果有看到就代表成功。

3.4 Nginx 轉發

現在我們都已經設定好了,但是還沒有將外部流量轉發到 bot 上面,所以 telegram 伺服器還沒有辦法與 bot 溝通。

我們 bot 會在 VM 的本地端運行。所以現在的流程是需要將 https://<VM_IP>.nip.io/callback 的流量轉發到 http://127.0.0.1:5000/callback ( Bot 的接口 ) 上面

1
2
cd /etc/nginx/sites-available
sudo vim default

在 server 的 { } 之中添加下面的程式

1
2
3
4
5
location /callback {
		proxy_pass http://127.0.0.1:5000;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
}
  • /callback 代表從哪個地方接收到的,要根據你 bot 的接口位置修改
  • proxy_pass 代表要轉發到的 address

最後,檢查 nginx 設定檔是否有錯誤並重啟

1
2
sudo nginx -t
sudo systemctl reload nginx

3.5 完成

現在全部的事情都完畢了,我們只需要執行 Bot 後,到 Telegram 中輸入 /start 就可以看到我們的機器人有正常的回覆了

現在我們的程式都是運行在 VM 上面,這樣除了不好維護之外也不易管理。所以,下一步我們就是要使用到 container 的技術來將我們的機器人運行到 Docker 上面。

Reference

  1. Deploy a simple flask app on GCP with Nginx – JIHONGO’s Blog
  2. Setup SSL Certificate for Website on Google Cloud Platform | by Max Shestov | Medium
  3. (一)一步步打造 Telegram Bot. 最近因為一些契機學了 Python 3,用它做了一個 Telegram… | by zaoldyeck | Medium
comments powered by Disqus