《赖田捕手:追加篇》第 35 天:制造 Deploy to Heroku 按钮

第 35 天:制造 Deploy to Heroku 按钮

我打开信封,有张明信片在里面。明信片封面是一个大大的 Heroku 标志,背面则写着:亲爱的小艾,伟大的学者路德维希·希洛库曾经这麽说过,当眼睛看到美丽的 APP 时,手就会想在键盘上为她做一个 Deploy to Heroku 按钮。我多麽希望我也能为你做出一个按钮。

~节录自《聊天机器人的历史:如果不会,那就》

延伸自系列文 《从LINE BOT到资料视觉化:赖田捕手》

《赖田捕手:追加篇》:

今天您将知道:

  1. 设计 LINE Notify 订阅流程
  2. 制造 Deploy to Heroku 快速按钮
  3. 使用 Deploy to Heroku 快速按钮

设计 LINE Notify 订阅流程

第 34 天当中,我们大致了解了 LINE Notify 的运作方式,以及程序码的实作细节。那麽今天就来介绍一下我们可以怎麽做,来达到将使用者与 LINE Notify 建立连结,并且透过 LINE Notify 发送讯息给指定使用者的这个功能。
要让使用者与 LINE Notify 建立连结,我们首先要传送一个授权网址 (Auth Link) 给使用者,而要产生这样子的授权网址,我们需要 Client ID、Redirect URI、以及一个我们自行定义的 State,如图一

https://ithelp.ithome.com.tw/upload/images/20210117/20120178hpdnRObiI9.png
图一、LINE BOT 产生授权网址

可以自行定义的 State 非常重要,因为之後 LINE Notify 给出来的连结权杖 (Access Token) 并没有关於使用者的任何资料,为了要知道哪一个连结权杖会对应到哪一个使用者,我们必须要用 State 来记录。因此这边就用 LINE 给出来的使用者代码来当作 State:

def create_auth_link(user_id, client_id=client_id, redirect_uri=redirect_uri):
    
    data = {
        'response_type': 'code', 
        'client_id': client_id, 
        'redirect_uri': redirect_uri, 
        'scope': 'notify', 
        'state': user_id
    }
    query_str = urllib.parse.urlencode(data)
    
    return f'https://notify-bot.line.me/oauth/authorize?{query_str}'

这边的user_id可以从event.source.user_id拿到,而client_idredirect_uri则是我们在 LINE Notify 上登录服务的时候会拿到/使用的资料。至於 LINE BOT 该在什麽时候将这个授权网址发送给使用者,大家就可以自行设计。以我们目前想要做的双色打光来说,如果要将双色打光处理过後的图片都由 LINE Notify 发送给使用者的话,那这个网址就必须放在使用者容易拿到的地方,或是每一次互动都发送这个网址,或是更贴心一点,只发送给没有与 LINE Notify 建立连结的使用者。
当使用者进入授权网址,点击同意并连结之後,我们的 LINE BOT 要能收到 LINE Notify 产生的 Code,以及授权网址本身的 State,如图二

https://ithelp.ithome.com.tw/upload/images/20210117/20120178SkKzlmzJEG.png
图二、LINE USER 同意并连结

这件事我们之前已经示范过该如何实作了,就不罗嗦,程序码如下:

  • app/routes.py
from flask import request

@app.route("/callback/notify", methods=['GET'])
def callback_notify():
    
    assert request.headers['referer'] == 'https://notify-bot.line.me/'
    code = request.args.get('code')
    state = request.args.get('state')

    # 接下来要实作的程序码    
    success = handle_subscribe(code, state)

    return '恭喜完成 LINE Notify 连动!请关闭此视窗。' 
  • 第六行:state = request.args.get('state')
    这边这个state,由於我们刻意安排的结果,其实就是使用者代码 (user_id)。

我们的 LINE BOT 拿到这个 Code,就可以向 LINE Notify 拿取最重要的连结权杖 (Access Token) 了 (图三):

import json

def get_token(code, client_id=client_id, client_secret=client_secret, redirect_uri=redirect_uri):
    url = 'https://notify-bot.line.me/oauth/token'
    headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
    data = {
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': redirect_uri,
        'client_id': client_id,
        'client_secret': client_secret
    }
    data = urllib.parse.urlencode(data).encode()
    req = urllib.request.Request(url, data=data, headers=headers)
    page = urllib.request.urlopen(req).read()
    
    res = json.loads(page.decode('utf-8'))
    return res['access_token']

https://ithelp.ithome.com.tw/upload/images/20210117/20120178i00rsF347c.png
图三、LINE BOT 取得连结权杖

我们得好好地把这个连结权杖储存起来。说道储存资料,第一个想法当然就是 Heroku Postgress!我们的 Heroku Postgres 已经有一个表格user_dualtone_settings用来储存使用者的双色打光设定,现在我们可以再建立一个表格,用来储存连结权杖 (当然,若大家想用相同的表格来储存这些资料也完全 OK,毕竟都可以用user_id来当作表格的 Primary Key)。那麽,这个表格要储存哪些资料呢:

  • app/custom_models/CallDatabase.py
def init_table():
    conn, cursor = access_database()
    postgres_table_query = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'"
    cursor.execute(postgres_table_query)
    table_records = cursor.fetchall()
    table_records = [i[0] for i in table_records]
    print('CallDatabase: table_records', table_records)

    # 用来储存连结权杖的表格
    if 'notify_subscription' not in table_records:
        create_table_query = """CREATE TABLE notify_subscription (
            user_id VARCHAR ( 50 ) PRIMARY KEY,
            access_token VARCHAR ( 50 ) NOT NULL,
            subscribe BOOLEAN NOT NULL
        );"""

        cursor.execute(create_table_query)
        conn.commit()

    return True

关於user_dualtone_settings的相关程序码之前介绍过了,这边我们就略过不看。我们将用来储存连结权杖的表格命名为notify_subscription,里面需要放入这些资料:

CREATE TABLE notify_subscription (
    user_id VARCHAR ( 50 ) PRIMARY KEY,
    access_token VARCHAR ( 50 ) NOT NULL,
    subscribe BOOLEAN NOT NULL
);
  • 第一行:user_id VARCHAR ( 50 ) PRIMARY KEY,
    我们用user_id当作 Primary Key。

  • 第二行:access_token VARCHAR ( 50 ) NOT NULL,
    并且存入access_token连结权杖。

  • 第三行:subscribe BOOLEAN NOT NULL
    最後一个则是这个连结是否仍然有效的布林值。若仍然有效,布林值为真。若已经失效,布林值为假。为什麽会有有效跟无效的差别呢?因为使用者随时可以解除 LINE Notify 的连结。这个时候我们的 LINE BOT 并不会收到任何通知,只有在尝试用授权连结发送讯息给使用者的时候,发送失败,才会知道原来使用者已经断开连结了。因此可以用一个栏位来追踪连结是否有效。

有了这个表格,当 LINE BOT 从 LINE Notify 拿到连结权杖之後,就可以存进来了 (图四):

  • app/custom_models/CallDatabase.py
def notify_subscribe(user_id, access_token, subscribe):
    conn, cursor = access_database()

    if notify_get_token(user_id):
        postgres_delete_query = "DELETE from notify_subscription where user_id = %s"
        cursor.execute(postgres_delete_query, (user_id, ))
        conn.commit()

    table_columns = '(user_id, access_token, subscribe)'
    postgres_insert_query = f"INSERT INTO notify_subscription {table_columns} VALUES (%s,%s,%s)"

    record = (user_id, access_token, subscribe)
    
    cursor.execute(postgres_insert_query, record)
    conn.commit()

    cursor.close()
    conn.close()
    
    return record

回头看看我们在app/routes.py当中,callback_notify这个函式里答应要实作的handle_subscribe

def handle_subscribe(code, user_id):
    access_token = get_token(code)
    _ = CallDatabase.notify_subscribe(user_id, access_token, True)

    return True

https://ithelp.ithome.com.tw/upload/images/20210117/20120178APKNAiTKDc.png
图四、Heroku Postgres 存入连结权杖

有没有觉得整个流程都配合起来的感觉了呢?
最後,当我们需要透过 LINE Notify 发送讯息给使用者的时候 (图五):

def send_message(user_id, access_token, im_url):

    url = 'https://notify-api.line.me/api/notify'
    headers = {"Authorization": "Bearer "+ access_token}
    data = {
        'message': '✌您点的双色打光已送达!', 
        'imageThumbnail': im_url, 
        'imageFullsize': im_url
    }
    data = urllib.parse.urlencode(data).encode()
    req = urllib.request.Request(url, data=data, headers=headers)
    
    try:
        page = urllib.request.urlopen(req).read()
        print('Notify Success!')

    except:
        _ = CallDatabase.notify_subscribe(user_id, access_token, False)
        print('Notify Fail. User Unsubscribe.')

https://ithelp.ithome.com.tw/upload/images/20210117/201201786AXuEnKxh9.png
图五、Heroku Postgres 读取连结权杖

这麽一来,我们的 LINE BOT 就可以透过 LINE Notify 在需要的时候主动推送讯息给指定使用者,而不需要担心预算爆表的问题了!

制造 Deploy to Heroku 快速按钮

经过扎实的五个星期,我们的双色打光 LINE BOT 终於完工了,草泥马们一定很开心,可以看着最爱的双色打光图片提振精神了。
但也许不是每位草泥马饲育家都像我们一样也身兼 LINE BOT 设计师。毕竟饲养草泥马本身就是一件艰困且耗时的工作,要是再要求每一位饲育家都能够略懂 Python,且了解该怎麽实作 LINE BOT,似乎有点太苛刻了。怎麽办呢?难道那些饲育家的草泥马,就永远跟双色打光的图片无缘了吗?
答案是否定的。
感谢 Heroku 推出的一个很棒的功能:【Deploy to Heroku 按钮】,让我们可以把自己设计出来的 APP 很快地分享给有兴趣的人,让大家不用重复相同的工作,每一次都重新开始。只要把自己的专案放到 Github 上,再加上一个 Deploy to Heroku 按钮,那麽下次想要写出一模一样的内容,只要按下这个按钮,不用写任何一行程序码,你也可以拥有双色打光 LINE BOT。是不是也很想知道该怎麽做出这个按钮呢?那麽我们现在就来看看吧!
首先要把我们用来架构 Heroku APP 的专案都搬到 Github 上。举例来说,以下是我们至今为止建立起的所有档案内容:

D:\appendix>tree /f
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│   runtime.txt
│   requirements.txt
│   Procfile
│   Alma.py
│
└───app
    │   __init__.py
    │   models_for_line.py
    │   routes.py 
    │
    └───custom_models
            AlmaTalks.py
            AlmaRenders.py
            AlmaNotify.py
            CallDatabase.py

我们要把这些档案内容,按照该有的档案架构,上传到我们的 Github 当中独立的资源库 (repository) 里。也就是在 Github 当中建立一个参考的资源库。完成以後,只要在该资源库的主目录底下,增加一个app.json档案,就算是做好准备工作了。这个app.json有点像是配置档,根据该档案的内容,Deploy to Heroku 按钮才知道布署新的 APP 时,该注意或该添加哪些扩充元件。那该怎麽写这个app.json呢:

{
  "name": "你-APP-的专案名称",
  "description": "你-APP-的专案描述",
  "repository": "你-APP-的资源库网址",
  "env": {
    "CHANNEL_ACCESS_TOKEN": "your channel access token",
    "CHANNEL_SECRET": "your channel secret",
    "YOUR_HEROKU_APP_NAME": "your Heroku App name", 
    "NOTIFY_CLIENT_ID": "your Notify client ID", 
    "NOTIFY_CLIENT_SECRET": "your Notify client secret"
  },
  "addons": ["heroku-postgresql:hobby-dev"],
  "success_url": "/"
}
  • 第二行:"name": "你-APP-的专案名称",
    我们 APP 的专案的名称,最多 30 字。可填可不填。

  • 第三行:"description": "你-APP-的专案描述",
    用来详细解释我们这个 APP 的用途,或是任何你想填入的话。可填可不填。

  • 第四行:"repository": "你-APP-的资源库网址",
    注记我们 APP 的专案是放在哪一个资源库当中。可填可不填。

  • 第五行:"env": {
    这个就重要了。这代表建立我们的 APP,需要用到的环境变数。记得把所有要用到的环境变数名称放进来,这样可以简化整个布署的流程。以我们目前的双色打光 LINE BOT 来说,需要用到的环境变数就包括用来验证 LINE BOT 身分的CHANNEL_ACCESS_TOKENCHANNEL_SECRET,用来验证 LINE Notify 身分的NOTIFY_CLIENT_IDNOTIFY_CLIENT_SECRET,以及方便我们路由做处理的YOUR_HEROKU_APP_NAME

  • 第十二行:"addons": ["heroku-postgresql:hobby-dev"],
    这个也很重要,表示我们这个 APP 需要用到哪些扩充元件。记得把所有要用到的扩充元件都放进来,方法是用阵列 (Array) 的方式把所有需要的扩充元件列下来。以我们目前的双色打光 LINE BOT 来说,只需要用到 Heroku Postgres,那阵列当中就放进这个元素,并且用:hooby-dev表示我们要用的方案名称。

  • 第十三行:"success_url": "/"
    当透过 Deploy to Heroku 按钮成功发布这个 APP 之後,会被重新导向的网址。可填可不填。

有没有发现很多都是可填可不填呢?不过记得要把envaddons这两个项目写对。关於app.json更详细的说明,可以参考 Heroku 的官方文件

都准备好了以後,就可以在存放我们 APP 详细内容的 Github 资源库当中,在README.md这个说明文件里添加 Deploy to Heroku 按钮了。使用方式如下:

  • README.md
# 详细表示法
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/你-Github-帐号/你-Github-资源库)

# 简易表示法
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)

详细表示法跟简易表示法的差别,在於详细表示法多了?template=https://github.com/你-Github-帐号/你-Github-资源库这个参数,因此这样子的按钮可以在 Github 资源库的README.md以外的地方使用。而简易表示法,因为没有特别指明是参照哪一个资源库,所以只能在 Github 资源库的README.md里面使用。其他更详细的资料,可以参考 Heroku 的官方文件

使用 Deploy to Heroku 快速按钮

因为我们这个 APP 跟 LINE BOT 以及 LINE Notify 有关,要真的能使用这个 APP,请一起准备好 LINE BOT 的CHANNEL_ACCESS_TOKENCHANNEL_SECRET,以及 LINE Notify 服务的NOTIFY_CLIENT_IDNOTIFY_CLIENT_SECRET。最後,当然,你要有一个 Heroku 帐号。都准备好了以後,就让我们来试用看看吧!

Deploy

点击 Deploy to Heroku 按钮之後,会带到Heroku 的产生新 APP 页面上,如图六

https://ithelp.ithome.com.tw/upload/images/20210117/20120178nRSRUIgyM9.png
图六、Create new APP

接着就看到我们在app.json填写的环境变数,这边会引导使用者将环境变数一个一个填好,如图七

https://ithelp.ithome.com.tw/upload/images/20210117/20120178vaUmQtT1rX.png
图七、Config Vars

填完之後,按下 Deploy,就开始布署新 APP 了,如图八

https://ithelp.ithome.com.tw/upload/images/20210117/20120178dXCdzRA3h9.png
图八、Build APP

等一阵子之後,看到布署成功的画面,如图九

https://ithelp.ithome.com.tw/upload/images/20210117/20120178VIkQujdtTi.png
图九、Deploy to Heroku

记得要改 LINE BOT 的 Webhook URL (图十),以及 LINE Notify 的 Callback URL (图十一)。

https://ithelp.ithome.com.tw/upload/images/20210117/20120178o3mUzZMnQR.png
图十、Webhook URL

https://ithelp.ithome.com.tw/upload/images/20210117/20120178wZAh8mk8pD.png
图十一、Callback URL

最後让我们一起试一试吧:

https://ithelp.ithome.com.tw/upload/images/20210117/20120178dlnKMg2zOS.jpg
图十二、今晚我想来点双色打光

https://ithelp.ithome.com.tw/upload/images/20210117/20120178D2CF2IOCTX.jpg
图十三、Hello from Alma!

耶,《赖田捕手:追加篇》的全部内容就到这边结束了,感谢大家的阅读。如果有兴趣,想要了解更多,比如说怎麽将相似的 LINE BOT 布署到 AWS 上,欢迎参考《LINE Bot by Python 全攻略》。非常感谢 iT 邦帮忙跟博硕文化,没有他们的帮忙,就不会有这本书。Happy Coding!


<<:  非线性回归-多项式回归 (polynomial regression in r)

>>:  [Cmoney 菁英软件工程师战斗营] IOS APP 菜鸟开发笔记----第七届菁英软件工程师战斗营结训心得

[DAY17]模板确认

TemplateSendMessage - ConfirmTemplate confirm_temp...

股票下单失败,出现讯息:全额预收

下单-买 Q:股票下单(买)失败,出现讯息:全额预收 A:问营业员,营业员说,可能该股票最近涨浮过大...

Day 15 Array

阵列Array 在程序设计中是非常常见的工具,当我们要建立多个相同型态的资料时,就会建立阵列,阵列的...

[C# WinForm] 建立第一个应用程序 Hello World

Visual Studio 是微软开发的整合开发环境(IDE),简称 VS。 VS 能开发的程序语言...

WhatsApp Business 商业帐号的独特功能

WhatsApp是世界上最多人使用的即时通讯软件,每月有20亿活跃用户,用户透过WhatsApp每天...